Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading