Skip to content

Commit

Permalink
Decode Multicall3 calldata recursively (#100)
Browse files Browse the repository at this point in the history
* Add tests for Multicall3 contract decoding

* Decode Multicall3 calldata recursively
  • Loading branch information
Ferossgp authored Sep 8, 2024
1 parent ff61db4 commit fddbd89
Show file tree
Hide file tree
Showing 57 changed files with 24,897 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/olive-keys-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': minor
---

Add support for decoding calldata recursively for Multicall3 contract
91 changes: 89 additions & 2 deletions packages/transaction-decoder/src/decoding/calldata-decode.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,86 @@
import { Effect } from 'effect'
import { isAddress, Abi, Hex } from 'viem'
import { isAddress, Hex } from 'viem'
import { getProxyStorageSlot } from './proxies.js'
import { getAndCacheAbi } from '../abi-loader.js'
import { AbiParams, AbiStore, ContractAbiResult, getAndCacheAbi, MissingABIError } from '../abi-loader.js'
import * as AbiDecoder from './abi-decode.js'
import { TreeNode } from '@/types.js'
import { PublicClient, RPCFetchError, UnknownNetwork } from '@/public-client.js'
import { sameAddress } from '../helpers/address.js'

// Same address on all supported chains https://www.multicall3.com/deployments
const MULTICALL3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11'

const decodeMulticall3 = (
params: TreeNode[],
chainID: number,
): Effect.Effect<
TreeNode[],
AbiDecoder.DecodeError | MissingABIError | RPCFetchError | UnknownNetwork,
AbiStore<AbiParams, ContractAbiResult> | PublicClient
> =>
Effect.gen(function* () {
const decodeCalls = params.map((par) =>
Effect.gen(function* () {
if (par.components != null) {
// NOTE: Iterate over tuples
const next = yield* Effect.all(
par.components.map((param) =>
Effect.gen(function* () {
if (param.components == null) return param
const target = param.components.find((p) => p.name === 'target')
const callData = param.components.find((p) => p.name === 'callData')

// NOTE: Found a tuple with calldata, recursively decode the calldata
if (target != null && callData != null && callData.value != null) {
const targetAddress = target.value as Hex

// NOTE: For nested failed calls we ignore the error as there could be contract that are not verified
const decoded = yield* decodeMethod({
data: callData.value as Hex,
chainID,
contractAddress: targetAddress,
}).pipe(Effect.orElseSucceed(() => null))

// Replace the call data with the decoded call data tree
const components = param.components.map((p) => {
if (p.name === 'callData') {
return {
...p,
value: decoded,
decoded: !!decoded,
}
}
return p
})

return {
...param,
components,
} as TreeNode
}

return param
}),
),
{
concurrency: 'unbounded',
},
)

return {
...par,
components: next,
}
} else {
return par
}
}),
)

return yield* Effect.all(decodeCalls, {
concurrency: 'unbounded',
})
})

export const decodeMethod = ({
data,
Expand Down Expand Up @@ -37,6 +115,15 @@ export const decodeMethod = ({
return yield* new AbiDecoder.DecodeError(`Failed to decode method: ${data}`)
}

if (sameAddress(MULTICALL3_ADDRESS, contractAddress) && decoded.params != null) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const deepDecodedParams = yield* decodeMulticall3(decoded.params!, chainID)
return {
...decoded,
params: deepDecodedParams,
}
}

return decoded
}).pipe(
Effect.withSpan('CalldataDecode.decodeMethod', {
Expand Down
20 changes: 20 additions & 0 deletions packages/transaction-decoder/test/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,29 @@ const AA_TRANSACTIONS = [
},
] as const

const MULTICALL3_TRANSACTIONS = [
{
hash: '0x548af97ffad9b36b4ec40b403299dda5fac222c130cf4a3e2c4d438d88fe2280',
chainID: 1,
},
{
hash: '0xd83d86917c0a4b67b73bebce6822bd2545ea69e98e15a054bf4458258fd6d068',
chainID: 1,
},
{
hash: '0xf821984218cb5f28807cbcf08c7b08bff1bd397d078af437905718a6cad93b50',
chainID: 1,
},
{
hash: '0xea1f1d20b3a22301f8c2c4191b6e85d9659a308e4fd877bfa6576434ba4c1451',
chainID: 1,
},
] as const

export const TEST_TRANSACTIONS: TXS = [
...NFTS_BLUR,
...AA_TRANSACTIONS,
...MULTICALL3_TRANSACTIONS,
{
hash: '0xde9f6210899218e17a3e71661ead5e16da228e168b0572b1ddc30a967968f8f6', // DAI
chainID: 1,
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-decoder/test/mocks/abi-loader-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const MockedAbiStoreLive = Layer.succeed(
Match.exhaustive,
)

yield* Effect.sync(() => fs.writeFileSync(`./test/mocks/abi/${key}.json`, JSON.stringify(value)))
yield* Effect.sync(() => fs.writeFileSync(`./test/mocks/abi/${key}.json`, value))
}),
get: ({ address, signature, event }) =>
Effect.gen(function* () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"address","name":"_logic","type":"address"},{"internalType":"address","name":"admin_","type":"address"},{"internalType":"bytes","name":"_data","type":"bytes"}],"stateMutability":"payable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"beacon","type":"address"}],"name":"BeaconUpgraded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"stateMutability":"payable","type":"fallback"},{"stateMutability":"payable","type":"receive"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"name":"changeAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"implementation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_implementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_implementation","type":"address"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"bytes20","name":"gitCommit","type":"bytes20"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"i","type":"uint256"},{"internalType":"bytes4","name":"action","type":"bytes4"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"ActionInvalid","type":"error"},{"inputs":[{"internalType":"uint256","name":"callbackInt","type":"uint256"}],"name":"CallbackNotSpent","type":"error"},{"inputs":[],"name":"ConfusedDeputy","type":"error"},{"inputs":[],"name":"ForwarderNotAllowed","type":"error"},{"inputs":[],"name":"InvalidOffset","type":"error"},{"inputs":[],"name":"InvalidTarget","type":"error"},{"inputs":[],"name":"NotConverged","type":"error"},{"inputs":[],"name":"PayerSpent","type":"error"},{"inputs":[{"internalType":"uint256","name":"callbackInt","type":"uint256"}],"name":"ReentrantCallback","type":"error"},{"inputs":[{"internalType":"bytes32","name":"oldWitness","type":"bytes32"}],"name":"ReentrantMetatransaction","type":"error"},{"inputs":[{"internalType":"address","name":"oldPayer","type":"address"}],"name":"ReentrantPayer","type":"error"},{"inputs":[{"internalType":"contract IERC20","name":"token","type":"address"},{"internalType":"uint256","name":"expected","type":"uint256"},{"internalType":"uint256","name":"actual","type":"uint256"}],"name":"TooMuchSlippage","type":"error"},{"inputs":[{"internalType":"uint8","name":"forkId","type":"uint8"}],"name":"UnknownForkId","type":"error"},{"inputs":[{"internalType":"bytes32","name":"oldWitness","type":"bytes32"}],"name":"WitnessNotSpent","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes20","name":"","type":"bytes20"}],"name":"GitCommit","type":"event"},{"stateMutability":"nonpayable","type":"fallback"},{"inputs":[{"components":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"contract IERC20","name":"buyToken","type":"address"},{"internalType":"uint256","name":"minAmountOut","type":"uint256"}],"internalType":"struct SettlerBase.AllowedSlippage","name":"slippage","type":"tuple"},{"internalType":"bytes[]","name":"actions","type":"bytes[]"},{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"msgSender","type":"address"},{"internalType":"bytes","name":"sig","type":"bytes"}],"name":"executeMetaTxn","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"previousAdmin","type":"address"},{"indexed":false,"internalType":"address","name":"newAdmin","type":"address"}],"name":"AdminChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"implementation","type":"address"}],"name":"Upgraded","type":"event"},{"stateMutability":"payable","type":"fallback"},{"inputs":[],"name":"admin","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"name":"changeAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"implementation","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_implementation","type":"address"}],"name":"upgradeTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_implementation","type":"address"},{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"upgradeToAndCall","outputs":[{"internalType":"bytes","name":"","type":"bytes"}],"stateMutability":"payable","type":"function"},{"stateMutability":"payable","type":"receive"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"address","name":"_libAddressManager","type":"address"},{"internalType":"string","name":"_implementationName","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"payable","type":"fallback"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"constant":false,"inputs":[{"internalType":"address","name":"receiver","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"syncState","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"counter","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"registrations","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"receiver","type":"address"}],"name":"register","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"}],"name":"NewRegistration","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"user","type":"address"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"}],"name":"RegistrationUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"indexed":true,"internalType":"address","name":"contractAddress","type":"address"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"StateSynced","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"uint256","name":"_chainId","type":"uint256"},{"components":[{"components":[{"internalType":"address","name":"facet","type":"address"},{"internalType":"enum Diamond.Action","name":"action","type":"uint8"},{"internalType":"bool","name":"isFreezable","type":"bool"},{"internalType":"bytes4[]","name":"selectors","type":"bytes4[]"}],"internalType":"struct Diamond.FacetCut[]","name":"facetCuts","type":"tuple[]"},{"internalType":"address","name":"initAddress","type":"address"},{"internalType":"bytes","name":"initCalldata","type":"bytes"}],"internalType":"struct Diamond.DiamondCutData","name":"_diamondCut","type":"tuple"}],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"payable","type":"fallback"}]
Loading

0 comments on commit fddbd89

Please sign in to comment.