This example involves two functions. The goal of the first function is to bridge tokens from one chain to another, and the goal of the second function is to invest in Aave the tokens received on the destination chain.
To achieve this, the bridge must complete before the investment begins. Mimic enables this kind of function concatenation by allowing one function to emit a custom event that triggers other functions in response.
The first function should include something as follows:
With this trigger configuration, the invest function will automatically execute every time an intent from user 0xUser emits an event containing 0xCustomId on Optimism.
import {
Arbitrum,
Base,
Bytes,
ChainId,
ERC20Token,
evm,
EvmEncodeParam,
Optimism,
SwapBuilder,
Token,
TokenAmount,
} from '@mimicprotocol/lib-ts'
import { inputs } from './types/bridge'
export default function main(): void {
const sourceChain = inputs.sourceChain
const destinationChain = inputs.destinationChain
if (sourceChain == destinationChain) throw new Error('Single-chain swap not supported')
const tokenIn = getUsdc(sourceChain)
const tokenAmountIn = TokenAmount.fromStringDecimal(tokenIn, inputs.amount)
const tokenOut = getUsdc(destinationChain)
const tokenAmountOut = TokenAmount.fromStringDecimal(tokenOut, inputs.minAmountOut)
const feeToken = new ERC20Token(inputs.feeToken, inputs.destinationChain)
const maxFee = TokenAmount.fromStringDecimal(feeToken, inputs.maxFee)
// Set topic hash and encode tokenOut address as extra data
const topic = evm.keccak('Bridged USDC')
const data = evm.encode([EvmEncodeParam.fromValue('address', tokenAmountOut.token.address)])
// Bridge tokens owned by the smart account
SwapBuilder.forChains(sourceChain, destinationChain)
.addTokenInFromTokenAmount(tokenAmountIn)
.addTokenOutFromTokenAmount(tokenAmountOut, inputs.smartAccount)
.addUser(inputs.smartAccount)
.addMaxFee(maxFee)
// Settler smart contract will emit an IntentExecuted event in which `topic2 = keccak256('Bridged USDC')`
.addEvent(Bytes.fromHexString(topic), Bytes.fromHexString(data)) // Optional
.build()
.send()
}
function getUsdc(chainId: i32): Token {
if (chainId == ChainId.ARBITRUM) return Arbitrum.USDC
if (chainId == ChainId.BASE) return Base.USDC
if (chainId == ChainId.OPTIMISM) return Optimism.USDC
throw new Error('Invalid chain')
}
version: 1.0.0
name: Bridge USDC and emit an event
description: Bridge USDC between Arbitrum, Base and Optimism, and emit a custom event to trigger another function
inputs:
- sourceChain: uint32
- destinationChain: uint32
- smartAccount: address # Important: The smart account should exist in both chains
- amount: string # e.g., '10.5' = 10.5 USDC
- minAmountOut: string # e.g., '10' = 10 USDC
- feeToken: address
- maxFee: string # e.g., '0.01' = 0.01 of the given token
import {
Address,
BigInt,
ChainId,
environment,
ERC20Token,
evm,
EvmCallBuilder,
EvmDecodeParam,
JSON,
TokenAmount,
TriggerType,
} from '@mimicprotocol/lib-ts'
import { inputs } from './types/invest'
import { AavePoolUtils } from './types/invest/AavePool'
import { ERC20Utils } from './types/invest/ERC20'
import { IntentExecutedEvent } from './types/invest/Settler'
export default function main(): void {
const chainId = inputs.chainId
const smartAccount = inputs.smartAccount
const aaveV3Pool = getAaveV3Pool(chainId)
const trigger = environment.getContext().triggerPayload
if (trigger.type != TriggerType.EVENT) throw new Error('Trigger not event')
const triggerData = trigger.getEventData()
const event = IntentExecutedEvent.decode(triggerData.topics, triggerData.eventData)
if (event.intent.user != smartAccount) throw new Error('Intent user not smart account')
// Get the token and the amount from the event emitted by the function that triggered this one
const tokenStr = evm.decode(new EvmDecodeParam('address', event.data.toHexString()))
const token = Address.fromString(tokenStr)
const amountsStr = evm.decode(new EvmDecodeParam('uint256[]', event.output.toHexString()))
const amounts = JSON.parse<string[]>(amountsStr)
if (amounts.length == 0) throw new Error('Empty amounts array')
const amount = BigInt.fromString(amounts[0])
const feeToken = new ERC20Token(inputs.feeToken, chainId)
const maxFee = TokenAmount.fromStringDecimal(feeToken, inputs.maxFee)
const approveData = ERC20Utils.encodeApprove(aaveV3Pool, amount)
const supplyData = AavePoolUtils.encodeSupply(token, amount, smartAccount, 0)
// Give approval and deposit the tokens, owned by the smart account
EvmCallBuilder.forChain(chainId)
.addCall(token, approveData)
.addCall(aaveV3Pool, supplyData)
.addUser(smartAccount)
.addMaxFee(maxFee)
.build()
.send()
}
function getAaveV3Pool(chainId: i32): Address {
if (chainId == ChainId.ARBITRUM) return Address.fromString('0x794a61358d6845594f94dc1db02a252b5b4814ad')
if (chainId == ChainId.BASE) return Address.fromString('0xa238dd80c259a72e81d7e4664a9801593f98d1c5')
if (chainId == ChainId.OPTIMISM) return Address.fromString('0x794a61358d6845594f94dc1db02a252b5b4814ad')
throw new Error('Invalid chain')
}
version: 1.0.0
name: Deposit USDC in Aave (event trigger)
description: Deposit in Aave USDC tokens that were bridged from another chain
inputs:
- chainId: uint32
- smartAccount: address
- feeToken: address
- maxFee: string # e.g., '0.01' = 0.01 of the given token
abis:
- AavePool: ./src/abis/AavePool.json
- ERC20: ./src/abis/ERC20.json
- Settler: ./src/abis/Settler.json
import { config } from 'dotenv'
config()
import { Chains, Client, EthersSigner, Sort, TriggerType } from '@mimicprotocol/sdk'
import { AbiCoder, Interface, keccak256, toUtf8Bytes } from 'ethers'
import { inc } from 'semver'
import SettlerAbi from './abis/Settler.json'
const SETTLER_IFACE = new Interface(SettlerAbi)
const INTENT_EXECUTED_TOPIC = SETTLER_IFACE.getEvent('IntentExecuted')!.topicHash
const BRIDGED_TOPIC = keccak256(toUtf8Bytes('Bridged USDC'))
const MIMIC_PROTOCOL_SETTLER = '0x609d831C0068844e11eF85a273c7F356212Fd6D1'
const USDT: Record<number, string> = {
10: '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', // Optimism
8453: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2', // Base
42161: '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58', // Arbitrum
}
async function main(): Promise<void> {
if (!process.env.PRIVATE_KEY) throw new Error('Missing PRIVATE_KEY in .env file')
if (!process.env.INVEST_CID) throw new Error('Missing INVEST_CID in .env file')
if (!process.env.SMART_ACCOUNT) throw new Error('Missing SMART_ACCOUNT in .env file')
const { PRIVATE_KEY, INVEST_CID, SMART_ACCOUNT } = process.env
// Create client with signer
const signer = EthersSigner.fromPrivateKey(PRIVATE_KEY)
const client = new Client({ signer })
// Get function manifest from deployed function
const manifest = await client.functions.getManifest(INVEST_CID)
// Set user topic to filter events
const USER_TOPIC = AbiCoder.defaultAbiCoder().encode(['address'], [SMART_ACCOUNT])
// Increment trigger version
const latestTrigger = await client.triggers.get({ functionCid: INVEST_CID, sort: Sort.desc, limit: 1 })
const version = latestTrigger.length > 0 ? inc(latestTrigger[0].version.split('-')[0], 'patch') : '0.0.1'
if (!version) throw new Error('Invalid trigger version')
// Set trigger based on a blockchain event
const config = {
type: TriggerType.Event,
contract: MIMIC_PROTOCOL_SETTLER,
topics: [
[INTENT_EXECUTED_TOPIC], // The event emitted by the Settler
[USER_TOPIC], // Important: To prevent other users from triggering this function
[BRIDGED_TOPIC], // Emitted by the bridge function
],
delta: '1h',
endDate: 0, // No end date
}
// Submit one function trigger per chain
const chainIds = [Chains.Arbitrum, Chains.Base, Chains.Optimism]
for (const chainId of chainIds) {
const input = {
chainId,
smartAccount: SMART_ACCOUNT,
feeToken: USDT[chainId],
maxFee: '0.3',
}
const trigger = await client.triggers.signAndCreate({
description: `Invest bridged amount in Aave (${chainId})`,
functionCid: INVEST_CID,
version: `${version}-${chainId}`,
manifest,
config: { ...config, chainId },
input,
executionFeeLimit: '0',
minValidations: 1,
})
console.log(`Created trigger on chain ${chainId}: ${trigger.sig}`)
}
}
main().catch((err) => {
console.error(err)
process.exit(1)
})