Skip to content

Commit bfcbb7d

Browse files
committed
Add recursive decoding for all calldata in params
1 parent b994dbc commit bfcbb7d

19 files changed

+4225
-52
lines changed

packages/transaction-decoder/src/decoding/calldata-decode.ts

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { isAddress, Hex, getAddress, encodeFunctionData, Address } from 'viem'
33
import { getProxyStorageSlot } from './proxies.js'
44
import { AbiParams, AbiStore, ContractAbiResult, getAndCacheAbi, MissingABIError } from '../abi-loader.js'
55
import * as AbiDecoder from './abi-decode.js'
6-
import { ProxyType, TreeNode } from '../types.js'
6+
import { TreeNode } from '../types.js'
77
import { PublicClient, RPCFetchError, UnknownNetwork } from '../public-client.js'
8-
import { sameAddress } from '../helpers/address.js'
9-
import { MULTICALL3_ADDRESS, SAFE_MULTISEND_ABI, SAFE_MULTISEND_SIGNATURE } from './constants.js'
8+
import { SAFE_MULTISEND_ABI, SAFE_MULTISEND_SIGNATURE } from './constants.js'
9+
10+
const callDataKeys = ['callData', 'data', '_data']
11+
const addressKeys = ['to', 'target', '_target']
1012

1113
const decodeBytesRecursively = (
1214
node: TreeNode,
@@ -18,8 +20,6 @@ const decodeBytesRecursively = (
1820
AbiStore<AbiParams, ContractAbiResult> | PublicClient
1921
> =>
2022
Effect.gen(function* () {
21-
const callDataKeys = ['callData', 'data']
22-
const addressKeys = ['to', 'target']
2323
const isCallDataNode =
2424
callDataKeys.includes(node.name) && node.type === 'bytes' && node.value && node.value !== '0x'
2525

@@ -147,20 +147,19 @@ export const decodeMethod = ({
147147
}) =>
148148
Effect.gen(function* () {
149149
const signature = data.slice(0, 10)
150-
let proxyType: ProxyType | undefined
150+
let implementationAddress: Address | undefined
151151

152152
if (isAddress(contractAddress)) {
153153
//if contract is a proxy, get the implementation address
154154
const implementation = yield* getProxyStorageSlot({ address: getAddress(contractAddress), chainID })
155155

156156
if (implementation) {
157-
contractAddress = implementation.address
158-
proxyType = implementation.type
157+
implementationAddress = implementation.address
159158
}
160159
}
161160

162161
const abi = yield* getAndCacheAbi({
163-
address: contractAddress,
162+
address: implementationAddress ?? contractAddress,
164163
signature,
165164
chainID,
166165
})
@@ -171,38 +170,6 @@ export const decodeMethod = ({
171170
return yield* new AbiDecoder.DecodeError(`Failed to decode method: ${data}`)
172171
}
173172

174-
//MULTICALL3: decode the params for the multicall3 contract
175-
if (sameAddress(MULTICALL3_ADDRESS, contractAddress) && decoded.params) {
176-
const targetAddress = decoded.params.find((p) => p.name === 'target')?.value as Address | undefined
177-
const decodedParams = yield* Effect.all(
178-
decoded.params.map((p) => decodeBytesRecursively(p, chainID, targetAddress)),
179-
{
180-
concurrency: 'unbounded',
181-
},
182-
)
183-
184-
return {
185-
...decoded,
186-
params: decodedParams,
187-
}
188-
}
189-
190-
//SAFE CONTRACT: decode the params for the safe smart account contract
191-
if (proxyType === 'safe' && decoded.params != null) {
192-
const toAddress = decoded.params.find((p) => p.name === 'to')?.value as Address | undefined
193-
const decodedParams = yield* Effect.all(
194-
decoded.params.map((p) => decodeBytesRecursively(p, chainID, toAddress)),
195-
{
196-
concurrency: 'unbounded',
197-
},
198-
)
199-
200-
return {
201-
...decoded,
202-
params: decodedParams,
203-
}
204-
}
205-
206173
//MULTISEND: decode the params for the multisend contract which is also related to the safe smart account
207174
if (
208175
decoded.signature === SAFE_MULTISEND_SIGNATURE &&
@@ -216,6 +183,29 @@ export const decodeMethod = ({
216183
}
217184
}
218185

186+
//Attempt to decode the params recursively if they contain data bytes or tuple params
187+
if (decoded.params != null) {
188+
const hasCalldataParam = decoded.params.find((p) => callDataKeys.includes(p.name) && p.type === 'bytes')
189+
const hasTuppleParams = decoded.params.some((p) => p.type === 'tuple')
190+
191+
if (hasCalldataParam || hasTuppleParams) {
192+
const targetAddressParam = decoded.params.find((p) => addressKeys.includes(p.name))
193+
const targetAddress = targetAddressParam?.value as Address | undefined
194+
195+
const decodedParams = yield* Effect.all(
196+
decoded.params.map((p) => decodeBytesRecursively(p, chainID, targetAddress)),
197+
{
198+
concurrency: 'unbounded',
199+
},
200+
)
201+
202+
return {
203+
...decoded,
204+
params: decodedParams,
205+
}
206+
}
207+
}
208+
219209
return decoded
220210
}).pipe(
221211
Effect.withSpan('CalldataDecode.decodeMethod', {

packages/transaction-decoder/test/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ const OTHER = [
150150
hash: '0x026fdb8b0017ef0e468e6d1627357adb9a8c4b6205ac0049bad80c253c76750c', // Disperse app
151151
chainID: 1,
152152
},
153+
{
154+
hash: '0x36f5c6d053ef3de0a412f871ead797d199d80dbc5ea4ba6ab1b1a211730aea13', //Uniswap Multicall
155+
chainID: 1,
156+
},
153157
] as const
154158

155159
export const TEST_TRANSACTIONS: TXS = [

packages/transaction-decoder/test/mocks/abi-loader-mock.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1+
/* eslint-disable turbo/no-undeclared-env-vars */
12
import { Effect, Layer, Match } from 'effect'
23
import fs from 'node:fs'
34
import { AbiStore } from '../../src/abi-loader.js'
4-
// import { FourByteStrategyResolver } from '../../src/effect.js'
5-
// import { EtherscanStrategyResolver } from '../../src/abi-strategy/index.js'
5+
import { FourByteStrategyResolver } from '../../src/effect.js'
6+
import { EtherscanStrategyResolver } from '../../src/abi-strategy/index.js'
67

78
export const MockedAbiStoreLive = Layer.succeed(
89
AbiStore,
910
AbiStore.of({
1011
strategies: {
11-
default: [
12-
// Run only when adding a new test case
13-
// EtherscanStrategyResolver({ apikey: '' }),
14-
// FourByteStrategyResolver(),
15-
],
12+
default:
13+
process.env.ETHERSCAN_API_KEY != null
14+
? [
15+
// Run only when adding a new test case
16+
EtherscanStrategyResolver({ apikey: process.env.ETHERSCAN_API_KEY! }),
17+
FourByteStrategyResolver(),
18+
]
19+
: [],
1620
},
1721
set: (key, response) =>
1822
Effect.gen(function* () {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"inputs":[{"internalType":"contract ENS","name":"_old","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"label","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"NewOwner","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"resolver","type":"address"}],"name":"NewResolver","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"NewTTL","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"node","type":"bytes32"},{"indexed":false,"internalType":"address","name":"owner","type":"address"}],"name":"Transfer","type":"event"},{"constant":true,"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"old","outputs":[{"internalType":"contract ENS","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"recordExists","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"resolver","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setApprovalForAll","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"address","name":"resolver","type":"address"}],"name":"setResolver","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"}],"name":"setSubnodeOwner","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"bytes32","name":"label","type":"bytes32"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"resolver","type":"address"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setSubnodeRecord","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"},{"internalType":"uint64","name":"ttl","type":"uint64"}],"name":"setTTL","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"bytes32","name":"node","type":"bytes32"}],"name":"ttl","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"payable":false,"stateMutability":"view","type":"function"}]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"transferFrom","type":"function","stateMutability":"nonpayable","inputs":[{"type":"address"},{"type":"address"},{"type":"address"},{"type":"uint256"}],"outputs":[]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"inputs":[],"name":"getCurrentBlockTimestamp","outputs":[{"internalType":"uint256","name":"timestamp","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getEthBalance","outputs":[{"internalType":"uint256","name":"balance","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"target","type":"address"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"bytes","name":"callData","type":"bytes"}],"internalType":"struct UniswapInterfaceMulticall.Call[]","name":"calls","type":"tuple[]"}],"name":"multicall","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"},{"components":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"uint256","name":"gasUsed","type":"uint256"},{"internalType":"bytes","name":"returnData","type":"bytes"}],"internalType":"struct UniswapInterfaceMulticall.Result[]","name":"returnData","type":"tuple[]"}],"stateMutability":"nonpayable","type":"function"}]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"validateUserOp","type":"function","stateMutability":"nonpayable","inputs":[{"type":"tuple","components":[{"type":"address"},{"type":"uint256"},{"type":"bytes"},{"type":"bytes"},{"type":"uint256"},{"type":"uint256"},{"type":"uint256"},{"type":"uint256"},{"type":"uint256"},{"type":"bytes"},{"type":"bytes"}]},{"type":"bytes32"},{"type":"uint256"}],"outputs":[]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"isModule","type":"function","stateMutability":"nonpayable","inputs":[{"type":"address"}],"outputs":[]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"enableTrading","type":"function","stateMutability":"nonpayable","inputs":[],"outputs":[]}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"name":"Transfer","type":"event","inputs":[{"type":"address"},{"type":"address"},{"type":"uint256"}]}

0 commit comments

Comments
 (0)