forked from koshikraj/ottopus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecode.test.ts
More file actions
252 lines (232 loc) · 9.61 KB
/
Copy pathdecode.test.ts
File metadata and controls
252 lines (232 loc) · 9.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import { type Abi, encodeFunctionData, maxUint256, parseAbi } from 'viem'
import { describe, expect, it } from 'vitest'
import type { Call } from '../core/index.js'
import { KNOWN_ABI } from './abi.js'
import { decodeCall, decodeCalls } from './decode.js'
import type { Lookups, SourcifyMatch } from './lookups.js'
/**
* The lookups answered from memory. What is under test is the order of
* trust and the shape of the answer, not Sourcify's uptime.
*/
const CHAIN = 'eip155:8453'
const USDC = '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'
const EOA = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045'
const WEIRD = '0x1111111111111111111111111111111111111111'
const SPENDER = '0x2222222222222222222222222222222222222222'
const CUSTOM_ABI = parseAbi(['function stake(uint256 amount, address onBehalfOf)'])
function fake(overrides: Partial<Lookups> = {}): Lookups {
const contracts: Record<string, SourcifyMatch | null> = {
[USDC]: { abi: KNOWN_ABI as Abi, name: 'FiatTokenV2_2', match: 'exact_match' },
[WEIRD]: null,
}
return {
async getCode(_chain, address) {
return address.toLowerCase() === EOA ? '0x' : '0x6080'
},
async sourcify(_chain, address) {
return contracts[address.toLowerCase()] ?? null
},
async fourByte() {
return []
},
async resolveName() {
return null
},
...overrides,
}
}
const call = (to: string, data: string, value = '0'): Call => ({
to: `${CHAIN}:${to}`,
value,
data: data.toLowerCase(),
chainId: CHAIN,
})
describe('decodeCall', () => {
it('reads a plain value transfer as native, and knows the target is a wallet', async () => {
const action = await decodeCall(call(EOA, '0x', '1000'), fake())
expect(action).toMatchObject({
source: 'native',
function: 'nativeTransfer()',
isContract: false,
verified: false,
value: '1000',
args: [],
})
})
it('names a verified contract that receives plain value, and warns on an unverified one', async () => {
const toUsdc = await decodeCall(call(USDC, '0x', '1'), fake())
expect(toUsdc).toMatchObject({ source: 'native', isContract: true, verified: true, contractName: 'FiatTokenV2_2' })
const toWeird = await decodeCall(call(WEIRD, '0x', '1'), fake())
expect(toWeird).toMatchObject({ source: 'native', isContract: true, verified: false })
})
it('decodes an ERC-20 transfer from the shipped ABI, verified by Sourcify', async () => {
const data = encodeFunctionData({ abi: KNOWN_ABI, functionName: 'transfer', args: [EOA, 500_000_000n] })
const action = await decodeCall(call(USDC, data), fake())
expect(action).toMatchObject({
source: 'abi',
verified: true,
isContract: true,
contractName: 'FiatTokenV2_2',
function: 'transfer(address,uint256)',
args: [
{ name: 'to', type: 'address', value: EOA },
{ name: 'amount', type: 'uint256', value: '500000000' },
],
})
expect(action.approval).toBeUndefined()
})
it('still decodes a known selector on an unverified contract, and says so', async () => {
const data = encodeFunctionData({ abi: KNOWN_ABI, functionName: 'transfer', args: [EOA, 1n] })
const action = await decodeCall(call(WEIRD, data), fake())
expect(action.source).toBe('abi')
expect(action.verified).toBe(false)
expect(action.contractName).toBeUndefined()
})
it('reads an approval, and calls the maximum unlimited', async () => {
const exact = encodeFunctionData({ abi: KNOWN_ABI, functionName: 'approve', args: [SPENDER, 42n] })
const infinite = encodeFunctionData({ abi: KNOWN_ABI, functionName: 'approve', args: [SPENDER, maxUint256] })
expect((await decodeCall(call(USDC, exact), fake())).approval).toEqual({
spender: `${CHAIN}:${SPENDER}`,
amount: '42',
})
expect((await decodeCall(call(USDC, infinite), fake())).approval).toEqual({
spender: `${CHAIN}:${SPENDER}`,
amount: 'unlimited',
})
})
it('reads setApprovalForAll(true) as an unlimited approval, and false as none', async () => {
const on = encodeFunctionData({ abi: KNOWN_ABI, functionName: 'setApprovalForAll', args: [SPENDER, true] })
const off = encodeFunctionData({ abi: KNOWN_ABI, functionName: 'setApprovalForAll', args: [SPENDER, false] })
expect((await decodeCall(call(USDC, on), fake())).approval?.amount).toBe('unlimited')
expect((await decodeCall(call(USDC, off), fake())).approval).toBeUndefined()
})
it('falls through to the verified ABI for a function we do not ship', async () => {
const data = encodeFunctionData({ abi: CUSTOM_ABI, functionName: 'stake', args: [7n, EOA] })
const lookups = fake({
async sourcify() {
return { abi: CUSTOM_ABI as Abi, name: 'Staking', match: 'match' }
},
})
const action = await decodeCall(call(WEIRD, data), lookups)
expect(action).toMatchObject({
source: 'sourcify',
verified: true,
contractName: 'Staking',
function: 'stake(uint256,address)',
args: [
{ name: 'amount', type: 'uint256', value: '7' },
{ name: 'onBehalfOf', type: 'address', value: EOA },
],
})
})
it('falls through to 4byte on an unverified contract, and is honest about it', async () => {
const data = encodeFunctionData({ abi: CUSTOM_ABI, functionName: 'stake', args: [7n, EOA] })
const lookups = fake({
async fourByte(selector) {
expect(selector).toBe(data.slice(0, 10))
// A collision first, then the real one — the collision fails to decode.
return ['not_parseable(', 'stake(uint256,address)']
},
})
const action = await decodeCall(call(WEIRD, data), lookups)
expect(action.source).toBe('4byte')
expect(action.verified).toBe(false)
expect(action.function).toBe('stake(uint256,address)')
})
/** vitalik.eth on Base has a 7702 delegation; a wallet with code is still a wallet. */
it('reads an EIP-7702 delegated wallet as a wallet, not a contract', async () => {
const lookups = fake({
async getCode() {
return `0xef0100${'ab'.repeat(20)}`
},
})
const action = await decodeCall(call(EOA, '0x', '1'), lookups)
expect(action.isContract).toBe(false)
})
/**
* The case that made every swap read "could not be decoded": LI.FI's router
* is an EIP-2535 diamond, Sourcify verifies the proxy, and the proxy's ABI
* carries no functions at all — while 4byte knows the selector perfectly
* well. A verified ABI that cannot speak for any function is not evidence
* about this call.
*/
it('asks 4byte about a verified proxy whose ABI has no functions', async () => {
const data = encodeFunctionData({ abi: CUSTOM_ABI, functionName: 'stake', args: [7n, EOA] })
const lookups = fake({
async sourcify() {
// A diamond façade: events and a fallback, and nothing callable.
return {
abi: [{ type: 'fallback', stateMutability: 'payable' }, { type: 'event', name: 'DiamondCut', inputs: [] }] as unknown as Abi,
name: 'LiFiDiamond',
match: 'match',
}
},
async fourByte() {
return ['stake(uint256,address)']
},
})
const action = await decodeCall(call(WEIRD, data), lookups)
expect(action).toMatchObject({
source: '4byte',
// Still a verified contract; only the name came from elsewhere.
verified: true,
contractName: 'LiFiDiamond',
function: 'stake(uint256,address)',
})
})
it('still says unknown for a proxy whose selector nobody knows', async () => {
const lookups = fake({
async sourcify() {
return { abi: [{ type: 'fallback', stateMutability: 'payable' }] as unknown as Abi, name: 'LiFiDiamond', match: 'match' }
},
})
const action = await decodeCall(call(WEIRD, '0xdeadbeef'), lookups)
expect(action).toMatchObject({ source: 'unknown', function: 'unknown', verified: true, contractName: 'LiFiDiamond' })
})
it('does not ask 4byte about a verified contract whose ABI lacks the selector', async () => {
let asked = false
const lookups = fake({
async sourcify() {
return { abi: CUSTOM_ABI as Abi, name: 'Staking', match: 'match' }
},
async fourByte() {
asked = true
return ['CodeIsLawZ95677371()']
},
})
const action = await decodeCall(call(WEIRD, '0xdeadbeef'), lookups)
expect(asked).toBe(false)
expect(action).toMatchObject({ source: 'unknown', function: 'unknown', verified: true, contractName: 'Staking' })
})
it('marks an unknown selector unknown rather than dropping the call', async () => {
const action = await decodeCall(call(WEIRD, '0xdeadbeef00000000'), fake())
expect(action).toMatchObject({ source: 'unknown', function: 'unknown', args: [], verified: false })
})
it('does not ask Sourcify about a wallet address', async () => {
let asked = false
const lookups = fake({
async sourcify() {
asked = true
return null
},
})
const action = await decodeCall(call(EOA, '0xa9059cbb'), lookups)
expect(asked).toBe(false)
expect(action.isContract).toBe(false)
})
it('refuses to guess when the chain cannot be read', async () => {
const lookups = fake({
async getCode() {
throw new Error('rpc down')
},
})
await expect(decodeCall(call(EOA, '0x', '1'), lookups)).rejects.toThrow('rpc down')
})
})
describe('decodeCalls', () => {
it('keeps one action per call, in order', async () => {
const transfer = encodeFunctionData({ abi: KNOWN_ABI, functionName: 'transfer', args: [EOA, 1n] })
const actions = await decodeCalls([call(EOA, '0x', '5'), call(USDC, transfer)], fake())
expect(actions.map((a) => a.source)).toEqual(['native', 'abi'])
})
})