forked from aEMPTYCUP/ottopus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecode.ts
More file actions
156 lines (138 loc) · 5.72 KB
/
Copy pathdecode.ts
File metadata and controls
156 lines (138 loc) · 5.72 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
import {
type Abi,
type AbiFunction,
type Hex,
decodeFunctionData,
maxUint256,
parseAbiItem,
toFunctionSelector,
toFunctionSignature,
} from 'viem'
import { type Call, type DecodedAction, accountOn, parseAccountId, parseChainId } from '../core/index.js'
import { KNOWN_BY_SELECTOR } from './abi.js'
import type { Lookups } from './lookups.js'
/**
* Calldata into something a person can check.
*
* Order of trust: ABIs we ship, then verified source from Sourcify, then a
* signature from 4byte, then "unknown" with the raw bytes left for the review
* page to show with a warning. Every action says which of those it came from,
* and whether the target is verified at all — a decoded name from 4byte on an
* unverified contract is a guess, and the page should say so.
*/
type DecodedArgs = readonly unknown[] | undefined
function stringify(value: unknown): string {
if (typeof value === 'bigint') return value.toString()
if (typeof value === 'string') return value.startsWith('0x') ? value.toLowerCase() : value
if (typeof value === 'boolean' || typeof value === 'number') return String(value)
return JSON.stringify(value, (_, v: unknown) => (typeof v === 'bigint' ? v.toString() : v))
}
function argsOf(item: AbiFunction, values: DecodedArgs) {
return item.inputs.map((input, i) => ({
name: input.name ?? `arg${i}`,
type: input.type,
value: stringify(values?.[i]),
}))
}
function tryDecode(abi: Abi, data: Hex): { item: AbiFunction; args: DecodedArgs } | null {
try {
const { functionName, args } = decodeFunctionData({ abi, data })
const selector = data.slice(0, 10)
const item = abi.find(
(f): f is AbiFunction =>
f.type === 'function' && f.name === functionName && toFunctionSelector(f) === selector,
)
return item ? { item, args } : null
} catch {
return null
}
}
/**
* An approval, if this is one. Unlimited means the maximum uint256, which is
* what every "infinite approval" button sends; anything smaller is a number a
* person can read and judge.
*/
function approvalOf(chainId: string, item: AbiFunction, args: DecodedArgs): DecodedAction['approval'] {
const chain = parseChainId(chainId)
const spender = (i: number) => accountOn(chain, String(args?.[i]))
switch (toFunctionSignature(item)) {
case 'approve(address,uint256)':
case 'increaseAllowance(address,uint256)': {
const amount = args?.[1] as bigint
return { spender: spender(0), amount: amount === maxUint256 ? 'unlimited' : amount.toString() }
}
case 'setApprovalForAll(address,bool)':
return args?.[1] === true ? { spender: spender(0), amount: 'unlimited' } : undefined
default:
return undefined
}
}
/**
* An EIP-7702 delegation designator: 0xef0100 followed by the delegate's
* address. A wallet that has one has code, but it is still a wallet — the
* person's own account, delegated to a known implementation — and reading it
* as an unverified contract would warn on every 7702 recipient.
*/
const DELEGATION = /^0xef0100[0-9a-f]{40}$/i
function hasContractCode(code: string): boolean {
return code.length > 2 && code !== '0x0' && !DELEGATION.test(code)
}
export async function decodeCall(call: Call, lookups: Lookups): Promise<DecodedAction> {
const { address } = parseAccountId(call.to)
const code = await lookups.getCode(call.chainId, address)
const isContract = hasContractCode(code)
const base = { target: call.to, isContract, value: call.value }
// Verification status is about the target, not the calldata: value sent to
// a contract with no data still lands in code someone may or may not have
// published, and the page should name it either way.
const source = isContract ? await lookups.sourcify(call.chainId, address) : null
const verified = source !== null
const named = source?.name === undefined ? {} : { contractName: source.name }
if (call.data === '0x' || call.data === '') {
return { ...base, ...named, source: 'native', verified, function: 'nativeTransfer()', args: [] }
}
const data = call.data as Hex
const selector = data.slice(0, 10)
const finish = (
from: DecodedAction['source'],
decoded: { item: AbiFunction; args: DecodedArgs },
): DecodedAction => {
const approval = approvalOf(call.chainId, decoded.item, decoded.args)
return {
...base,
...named,
source: from,
verified,
function: toFunctionSignature(decoded.item),
args: argsOf(decoded.item, decoded.args),
...(approval ? { approval } : {}),
}
}
const known = KNOWN_BY_SELECTOR.get(selector)
if (known) {
const decoded = tryDecode([known], data)
if (decoded) return finish('abi', decoded)
}
if (source) {
const decoded = tryDecode(source.abi, data)
if (decoded) return finish('sourcify', decoded)
// Verified source with no such function: the call hits a fallback, or
// nothing. A 4byte name here would be a collision dressed as a decoding,
// and worse than "unknown" because it looks like an answer.
return { ...base, ...named, source: 'unknown', verified, function: 'unknown', args: [] }
}
for (const signature of await lookups.fourByte(selector)) {
try {
const item = parseAbiItem(`function ${signature}`) as AbiFunction
const decoded = tryDecode([item], data)
if (decoded) return finish('4byte', decoded)
} catch {
// A signature 4byte holds that viem cannot parse is not one we can use.
}
}
return { ...base, ...named, source: 'unknown', verified, function: 'unknown', args: [] }
}
/** One action per call, in order. The policy layer relies on that pairing. */
export async function decodeCalls(calls: readonly Call[], lookups: Lookups): Promise<DecodedAction[]> {
return Promise.all(calls.map((call) => decodeCall(call, lookups)))
}