Bridge and invest in Aave

This example involves two tasks. The goal of the first task is to bridge tokens from one chain to another, and the goal of the second task 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 task concatenation by allowing one task to emit a custom event that triggers other tasks in response.

The first task should include something as follows:

SwapBuilder.addEvent('0xCustomId', '0xDataForTheOtherTask')

And the configuration for the second task should look like:

const trigger = {
  type: TriggerType.Event
  chainId: Chains.Optimism,
  contract: MIMIC_PROTOCOL_SETTLER,
  topics: [
    [INTENT_EXECUTED_TOPIC],
    [encode('0xUser')],
    ['0xCustomId'],
  ],
}

await client.configs.signAndCreate({ ..., trigger })

With this configuration, the invest task will automatically execute every time an intent from user 0xUser emits an event containing 0xCustomId on Optimism.

1. Bridge

Task

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')
}

Manifest

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 task
inputs:
  - sourceChain: uint32
  - destinationChain: uint32
  - smartAccount: address # Important: The smart account should exist in both chains
  - amount: string
  - minAmountOut: string
  - feeToken: address
  - maxFee: string

2. Invest

Task

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().trigger
  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 task 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')
}

Manifest

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
abis:
  - AavePool: ./src/abis/AavePool.json
  - ERC20: ./src/abis/ERC20.json
  - Settler: ./src/abis/Settler.json

Backend

import { config } from 'dotenv'
config()

import { Chains, Client, EthersSigner, Sort, TriggerType } from '@mimicprotocol/sdk'
import { AbiCoder, Interface, keccak256, toUtf8Bytes, Wallet } 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 task manifest from deployed task
  const manifest = await client.tasks.getManifest(INVEST_CID)

  // Set user topic to filter events
  const USER_TOPIC = AbiCoder.defaultAbiCoder().encode(['address'], [SMART_ACCOUNT])

  // Increment config version
  const latestConfig = await client.configs.get({ taskCid: INVEST_CID, sort: Sort.desc, limit: 1 })
  const version = latestConfig.length > 0 ? inc(latestConfig[0].version.split('-')[0], 'patch') : '0.0.1'
  if (!version) throw new Error('Invalid config version')

  // Set trigger based on a blockchain event
  const trigger = {
    type: TriggerType.Event as const,
    contract: MIMIC_PROTOCOL_SETTLER,
    topics: [
      [INTENT_EXECUTED_TOPIC], // The event emitted by the Settler
      [USER_TOPIC], // Important: To prevent other users from triggering this task
      [BRIDGED_TOPIC], // Emitted by the bridge task
    ],
    delta: '1h',
    endDate: 0, // No end date
  }

  // Submit one task config 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 config = await client.configs.signAndCreate({
      description: `Invest bridged amount in Aave (${chainId})`,
      taskCid: INVEST_CID,
      version: `${version}-${chainId}`,
      manifest,
      trigger: { ...trigger, chainId },
      input,
      executionFeeLimit: '0',
      minValidations: 1,
      signer: new Wallet(PRIVATE_KEY).address,
    })
    console.log(`Created config on chain ${chainId}: ${config.sig}`)
  }
}

main().catch((err) => {
  console.error(err)
  process.exit(1)
})

The ABIs used for this example can be downloaded below:

45KB
Open
4KB
Open
32KB
Open

Last updated