@bananapus/nana-sdk-core 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/constants.d.ts +1 -13
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js +4 -22
- package/dist/cjs/constants.js.map +1 -1
- package/dist/cjs/generated/abi/jbBuybackHookAbi.d.ts +1187 -0
- package/dist/cjs/generated/abi/jbBuybackHookAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbBuybackHookAbi.js +841 -0
- package/dist/cjs/generated/abi/jbBuybackHookAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbBuybackHookRegistryAbi.d.ts +791 -0
- package/dist/cjs/generated/abi/jbBuybackHookRegistryAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbBuybackHookRegistryAbi.js +632 -0
- package/dist/cjs/generated/abi/jbBuybackHookRegistryAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbControllerAbi.d.ts +2569 -0
- package/dist/cjs/generated/abi/jbControllerAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbControllerAbi.js +1906 -0
- package/dist/cjs/generated/abi/jbControllerAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbDirectoryAbi.d.ts +410 -0
- package/dist/cjs/generated/abi/jbDirectoryAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbDirectoryAbi.js +365 -0
- package/dist/cjs/generated/abi/jbDirectoryAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbMultiTerminalAbi.d.ts +1863 -0
- package/dist/cjs/generated/abi/jbMultiTerminalAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbMultiTerminalAbi.js +1459 -0
- package/dist/cjs/generated/abi/jbMultiTerminalAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbPermissionsAbi.d.ts +240 -0
- package/dist/cjs/generated/abi/jbPermissionsAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbPermissionsAbi.js +164 -0
- package/dist/cjs/generated/abi/jbPermissionsAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbProjectsAbi.d.ts +626 -0
- package/dist/cjs/generated/abi/jbProjectsAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbProjectsAbi.js +470 -0
- package/dist/cjs/generated/abi/jbProjectsAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbRouterTerminalGatewayAbi.d.ts +1503 -0
- package/dist/cjs/generated/abi/jbRouterTerminalGatewayAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbRouterTerminalGatewayAbi.js +1037 -0
- package/dist/cjs/generated/abi/jbRouterTerminalGatewayAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbRouterTerminalRegistryAbi.d.ts +1119 -0
- package/dist/cjs/generated/abi/jbRouterTerminalRegistryAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbRouterTerminalRegistryAbi.js +925 -0
- package/dist/cjs/generated/abi/jbRouterTerminalRegistryAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbSplitsAbi.d.ts +244 -0
- package/dist/cjs/generated/abi/jbSplitsAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbSplitsAbi.js +198 -0
- package/dist/cjs/generated/abi/jbSplitsAbi.js.map +1 -0
- package/dist/cjs/generated/abi/jbTokensAbi.d.ts +602 -0
- package/dist/cjs/generated/abi/jbTokensAbi.d.ts.map +1 -0
- package/dist/cjs/generated/abi/jbTokensAbi.js +501 -0
- package/dist/cjs/generated/abi/jbTokensAbi.js.map +1 -0
- package/dist/cjs/generated/juicebox.d.ts +25405 -36537
- package/dist/cjs/generated/juicebox.d.ts.map +1 -1
- package/dist/cjs/generated/juicebox.js +14362 -22761
- package/dist/cjs/generated/juicebox.js.map +1 -1
- package/dist/cjs/pureConstants.d.ts +15 -0
- package/dist/cjs/pureConstants.d.ts.map +1 -0
- package/dist/cjs/pureConstants.js +28 -0
- package/dist/cjs/pureConstants.js.map +1 -0
- package/dist/cjs/review/decode.d.ts.map +1 -1
- package/dist/cjs/review/decode.js +35 -51
- package/dist/cjs/review/decode.js.map +1 -1
- package/dist/cjs/review/relayr.d.ts +380 -0
- package/dist/cjs/review/relayr.d.ts.map +1 -0
- package/dist/cjs/review/relayr.js +899 -0
- package/dist/cjs/review/relayr.js.map +1 -0
- package/dist/cjs/safe.d.ts +376 -8
- package/dist/cjs/safe.d.ts.map +1 -1
- package/dist/cjs/safe.js +1088 -29
- package/dist/cjs/safe.js.map +1 -1
- package/dist/cjs/safeService.d.ts +361 -1
- package/dist/cjs/safeService.d.ts.map +1 -1
- package/dist/cjs/safeService.js +844 -6
- package/dist/cjs/safeService.js.map +1 -1
- package/dist/cjs/untrusted.d.ts +8 -0
- package/dist/cjs/untrusted.d.ts.map +1 -0
- package/dist/cjs/untrusted.js +33 -0
- package/dist/cjs/untrusted.js.map +1 -0
- package/dist/cjs/v6/distributions.d.ts +90 -0
- package/dist/cjs/v6/distributions.d.ts.map +1 -0
- package/dist/cjs/v6/distributions.js +287 -0
- package/dist/cjs/v6/distributions.js.map +1 -0
- package/dist/cjs/v6/index.d.ts +1 -0
- package/dist/cjs/v6/index.d.ts.map +1 -1
- package/dist/cjs/v6/index.js +1 -0
- package/dist/cjs/v6/index.js.map +1 -1
- package/dist/cjs/v6/permissionIds.d.ts +62 -0
- package/dist/cjs/v6/permissionIds.d.ts.map +1 -0
- package/dist/cjs/v6/permissionIds.js +70 -0
- package/dist/cjs/v6/permissionIds.js.map +1 -0
- package/dist/cjs/v6/permissions.d.ts +3 -62
- package/dist/cjs/v6/permissions.d.ts.map +1 -1
- package/dist/cjs/v6/permissions.js +6 -67
- package/dist/cjs/v6/permissions.js.map +1 -1
- package/dist/cjs/v6/splits.d.ts +1 -1
- package/dist/cjs/v6/splits.d.ts.map +1 -1
- package/dist/cjs/v6/splits.js +3 -3
- package/dist/cjs/v6/splits.js.map +1 -1
- package/dist/cjs/v6/sticky.d.ts +2 -2
- package/dist/cjs/v6/sticky.d.ts.map +1 -1
- package/dist/cjs/v6/types.d.ts +1 -1
- package/dist/cjs/v6/types.d.ts.map +1 -1
- package/dist/esm/constants.d.ts +1 -13
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.js +1 -21
- package/dist/esm/constants.js.map +1 -1
- package/dist/esm/generated/abi/jbBuybackHookAbi.d.ts +1187 -0
- package/dist/esm/generated/abi/jbBuybackHookAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbBuybackHookAbi.js +838 -0
- package/dist/esm/generated/abi/jbBuybackHookAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbBuybackHookRegistryAbi.d.ts +791 -0
- package/dist/esm/generated/abi/jbBuybackHookRegistryAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbBuybackHookRegistryAbi.js +629 -0
- package/dist/esm/generated/abi/jbBuybackHookRegistryAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbControllerAbi.d.ts +2569 -0
- package/dist/esm/generated/abi/jbControllerAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbControllerAbi.js +1903 -0
- package/dist/esm/generated/abi/jbControllerAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbDirectoryAbi.d.ts +410 -0
- package/dist/esm/generated/abi/jbDirectoryAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbDirectoryAbi.js +362 -0
- package/dist/esm/generated/abi/jbDirectoryAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbMultiTerminalAbi.d.ts +1863 -0
- package/dist/esm/generated/abi/jbMultiTerminalAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbMultiTerminalAbi.js +1456 -0
- package/dist/esm/generated/abi/jbMultiTerminalAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbPermissionsAbi.d.ts +240 -0
- package/dist/esm/generated/abi/jbPermissionsAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbPermissionsAbi.js +161 -0
- package/dist/esm/generated/abi/jbPermissionsAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbProjectsAbi.d.ts +626 -0
- package/dist/esm/generated/abi/jbProjectsAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbProjectsAbi.js +467 -0
- package/dist/esm/generated/abi/jbProjectsAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbRouterTerminalGatewayAbi.d.ts +1503 -0
- package/dist/esm/generated/abi/jbRouterTerminalGatewayAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbRouterTerminalGatewayAbi.js +1034 -0
- package/dist/esm/generated/abi/jbRouterTerminalGatewayAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbRouterTerminalRegistryAbi.d.ts +1119 -0
- package/dist/esm/generated/abi/jbRouterTerminalRegistryAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbRouterTerminalRegistryAbi.js +922 -0
- package/dist/esm/generated/abi/jbRouterTerminalRegistryAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbSplitsAbi.d.ts +244 -0
- package/dist/esm/generated/abi/jbSplitsAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbSplitsAbi.js +195 -0
- package/dist/esm/generated/abi/jbSplitsAbi.js.map +1 -0
- package/dist/esm/generated/abi/jbTokensAbi.d.ts +602 -0
- package/dist/esm/generated/abi/jbTokensAbi.d.ts.map +1 -0
- package/dist/esm/generated/abi/jbTokensAbi.js +498 -0
- package/dist/esm/generated/abi/jbTokensAbi.js.map +1 -0
- package/dist/esm/generated/juicebox.d.ts +25405 -36537
- package/dist/esm/generated/juicebox.d.ts.map +1 -1
- package/dist/esm/generated/juicebox.js +14634 -23044
- package/dist/esm/generated/juicebox.js.map +1 -1
- package/dist/esm/pureConstants.d.ts +15 -0
- package/dist/esm/pureConstants.d.ts.map +1 -0
- package/dist/esm/pureConstants.js +25 -0
- package/dist/esm/pureConstants.js.map +1 -0
- package/dist/esm/review/decode.d.ts.map +1 -1
- package/dist/esm/review/decode.js +21 -37
- package/dist/esm/review/decode.js.map +1 -1
- package/dist/esm/review/relayr.d.ts +380 -0
- package/dist/esm/review/relayr.d.ts.map +1 -0
- package/dist/esm/review/relayr.js +874 -0
- package/dist/esm/review/relayr.js.map +1 -0
- package/dist/esm/safe.d.ts +376 -8
- package/dist/esm/safe.d.ts.map +1 -1
- package/dist/esm/safe.js +1070 -29
- package/dist/esm/safe.js.map +1 -1
- package/dist/esm/safeService.d.ts +361 -1
- package/dist/esm/safeService.d.ts.map +1 -1
- package/dist/esm/safeService.js +818 -5
- package/dist/esm/safeService.js.map +1 -1
- package/dist/esm/untrusted.d.ts +8 -0
- package/dist/esm/untrusted.d.ts.map +1 -0
- package/dist/esm/untrusted.js +28 -0
- package/dist/esm/untrusted.js.map +1 -0
- package/dist/esm/v6/distributions.d.ts +90 -0
- package/dist/esm/v6/distributions.d.ts.map +1 -0
- package/dist/esm/v6/distributions.js +283 -0
- package/dist/esm/v6/distributions.js.map +1 -0
- package/dist/esm/v6/index.d.ts +1 -0
- package/dist/esm/v6/index.d.ts.map +1 -1
- package/dist/esm/v6/index.js +1 -0
- package/dist/esm/v6/index.js.map +1 -1
- package/dist/esm/v6/permissionIds.d.ts +62 -0
- package/dist/esm/v6/permissionIds.d.ts.map +1 -0
- package/dist/esm/v6/permissionIds.js +66 -0
- package/dist/esm/v6/permissionIds.js.map +1 -0
- package/dist/esm/v6/permissions.d.ts +3 -62
- package/dist/esm/v6/permissions.d.ts.map +1 -1
- package/dist/esm/v6/permissions.js +2 -64
- package/dist/esm/v6/permissions.js.map +1 -1
- package/dist/esm/v6/splits.d.ts +1 -1
- package/dist/esm/v6/splits.d.ts.map +1 -1
- package/dist/esm/v6/splits.js +1 -1
- package/dist/esm/v6/splits.js.map +1 -1
- package/dist/esm/v6/sticky.d.ts +2 -2
- package/dist/esm/v6/sticky.d.ts.map +1 -1
- package/dist/esm/v6/types.d.ts +1 -1
- package/dist/esm/v6/types.d.ts.map +1 -1
- package/package.json +6 -1
package/dist/esm/safe.js
CHANGED
|
@@ -1,11 +1,89 @@
|
|
|
1
|
-
import { concatHex, decodeFunctionData, decodeFunctionResult, encodeFunctionData, encodeFunctionResult, encodePacked, getAddress, getContractAddress, isAddress, isAddressEqual, keccak256,
|
|
1
|
+
import { concatHex, decodeAbiParameters, decodeFunctionData, decodeFunctionResult, encodeAbiParameters, encodeFunctionData, encodeFunctionResult, encodePacked, getAddress, getContractAddress, isAddress, isAddressEqual, keccak256, size, toHex, zeroAddress, } from "viem";
|
|
2
|
+
import { simulateStateChangingTransaction } from "./review/simulation.js";
|
|
3
|
+
import { isHexBytes, uint256 } from "./untrusted.js";
|
|
4
|
+
// This module runs nothing when it loads: every value below is a literal, so
|
|
5
|
+
// importing one export does not pull in the rest. Checksummed addresses are
|
|
6
|
+
// written out rather than computed with getAddress, and ABIs are JSON rather
|
|
7
|
+
// than parseAbi strings.
|
|
2
8
|
// Canonical Safe 1.4.1 deployments, from safe-global/safe-deployments.
|
|
3
9
|
// A fixed singleton and initializer preserve CREATE2 addresses across chains.
|
|
4
|
-
export const SAFE_FACTORY =
|
|
5
|
-
export const SAFE_SINGLETON =
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
export const SAFE_FACTORY = "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67";
|
|
11
|
+
export const SAFE_SINGLETON = "0x41675C099F32341bf84BFc5382aF534df5C7461a";
|
|
12
|
+
/** The 1.4.1 SafeL2 singleton, which SafeToL2Setup installs off Ethereum. */
|
|
13
|
+
const SAFE_L2_SINGLETON = "0x29fcB43b46531BcA003ddC8FCB67FFE91900C762";
|
|
14
|
+
export const SAFE_FALLBACK = "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99";
|
|
15
|
+
export const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
8
16
|
export const MAX_SAFE_OWNERS = 50;
|
|
17
|
+
/**
|
|
18
|
+
* Safe's canonical 1.4.1 creation calls `SafeToL2Setup.setupToL2` as `setup`'s
|
|
19
|
+
* delegatecall hook. That library repoints slot zero at SafeL2 on every chain
|
|
20
|
+
* except Ethereum, so one initializer produces the same address with a
|
|
21
|
+
* different, but paired, singleton per chain. Same address on every chain.
|
|
22
|
+
*/
|
|
23
|
+
export const SAFE_TO_L2_SETUP_ADDRESS = "0xBD89A1CE4DDe368FFAB0eC35506eEcE0b1fFdc54";
|
|
24
|
+
/** keccak256 of SafeToL2Setup's runtime, the same on every canonical chain. */
|
|
25
|
+
export const SAFE_TO_L2_SETUP_CODE_HASH = "0x2f25df28caf984366ee584e13241707e85dcd5a6ea0c14267928dafc1fd6274b";
|
|
26
|
+
/**
|
|
27
|
+
* Safe's vanity `paymentReceiver` marker. Inert, because a replayable
|
|
28
|
+
* initializer must still set a zero `payment`.
|
|
29
|
+
*/
|
|
30
|
+
export const SAFE_CANONICAL_PAYMENT_RECEIVER = "0x5afe7A11E7000000000000000000000000000000";
|
|
31
|
+
/**
|
|
32
|
+
* The official Safe releases an authority may run, from
|
|
33
|
+
* safe-global/safe-deployments: each singleton (Safe and SafeL2, canonical and
|
|
34
|
+
* EIP-155 deployments) and proxy factory. An address is pinned to its code by
|
|
35
|
+
* its deterministic deployment, so the list is a code allow-list. A contract
|
|
36
|
+
* that only implements the owner API is never a Safe. That holds on every
|
|
37
|
+
* supported chain; pin code hashes before adding a chain where an address does
|
|
38
|
+
* not pin its code, such as zkSync.
|
|
39
|
+
*/
|
|
40
|
+
export const RECOGNIZED_SAFE_RELEASES = [
|
|
41
|
+
{
|
|
42
|
+
version: "1.3.0",
|
|
43
|
+
singletons: [
|
|
44
|
+
"0xd9Db270c1B5E3Bd161E8c8503c55cEABeE709552",
|
|
45
|
+
"0x69f4D1788e39c87893C980c06EdF4b7f686e2938",
|
|
46
|
+
"0x3E5c63644E683549055b9Be8653de26E0B4CD36E",
|
|
47
|
+
"0xfb1bffC9d739B8D520DaF37dF666da4C687191EA",
|
|
48
|
+
],
|
|
49
|
+
factories: [
|
|
50
|
+
"0xa6B71E26C5e0845f74c812102Ca7114b6a896AB2",
|
|
51
|
+
"0xC22834581EbC8527d974F8a1c97E1bEA4EF910BC",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
version: "1.4.1",
|
|
56
|
+
singletons: [SAFE_SINGLETON, SAFE_L2_SINGLETON],
|
|
57
|
+
factories: [SAFE_FACTORY],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* keccak256 of the proxy runtime each recognized factory deploys
|
|
62
|
+
* (`proxyCreationCode()` of 1.3.0 and 1.4.1).
|
|
63
|
+
*/
|
|
64
|
+
const PROXY_CODE_HASH = "0xd7d408ebcd99b2b70be43e20253d6d92a8ea8fab29bd3be7f55b10032331fb4c";
|
|
65
|
+
const RECOGNIZED_SAFE_PROXY_CODE_HASHES = [
|
|
66
|
+
"0xb89c1b3bdf2cf8827818646bce9a8f6e372885f8c55e5c07acbd307cb133b000",
|
|
67
|
+
PROXY_CODE_HASH,
|
|
68
|
+
];
|
|
69
|
+
/** Each release's Ethereum singleton and the SafeL2 singleton SafeToL2Setup installs elsewhere. */
|
|
70
|
+
export const SAFE_L1_L2_SINGLETON_PAIRS = [
|
|
71
|
+
[SAFE_SINGLETON, SAFE_L2_SINGLETON],
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* Safe's MultiSendCallOnly 1.3.0, the same address on every supported chain. A
|
|
75
|
+
* Safe DELEGATECALLs it to run several calls as one transaction.
|
|
76
|
+
*/
|
|
77
|
+
export const MULTI_SEND_CALL_ONLY = "0x40A2aCCbd92BCA938b02010E17A5b8929b49130D";
|
|
78
|
+
export const MULTI_SEND_ABI = [
|
|
79
|
+
{
|
|
80
|
+
name: "multiSend",
|
|
81
|
+
type: "function",
|
|
82
|
+
stateMutability: "payable",
|
|
83
|
+
inputs: [{ type: "bytes", name: "transactions" }],
|
|
84
|
+
outputs: [],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
9
87
|
/**
|
|
10
88
|
* `SafeProxyFactory.proxyCreationCode()` at {@link SAFE_FACTORY}: the same
|
|
11
89
|
* bytes wherever the canonical 1.4.1 factory is deployed. Pinned so an address
|
|
@@ -14,6 +92,19 @@ export const MAX_SAFE_OWNERS = 50;
|
|
|
14
92
|
* be deployed; this constant is for reading a signed call back.
|
|
15
93
|
*/
|
|
16
94
|
export const SAFE_PROXY_CREATION_CODE = "0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f7669646564";
|
|
95
|
+
/**
|
|
96
|
+
* `GnosisSafeProxyFactory.proxyCreationCode()` of both recognized 1.3.0
|
|
97
|
+
* factories (canonical and EIP-155), which are byte-identical. With
|
|
98
|
+
* {@link SAFE_PROXY_CREATION_CODE} it lets a creation record be checked
|
|
99
|
+
* against its address from calldata alone.
|
|
100
|
+
*/
|
|
101
|
+
const SAFE_130_PROXY_CREATION_CODE = "0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea2646970667358221220d1429297349653a4918076d650332de1a1068c5f3e07c5c82360c277770b955264736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f7669646564";
|
|
102
|
+
/** The proxy creation code a recognized `factory` deploys. */
|
|
103
|
+
function proxyCreationCodeOf(factory) {
|
|
104
|
+
return RECOGNIZED_SAFE_RELEASES.find(({ factories }) => factories.some((candidate) => isAddressEqual(candidate, factory)))?.version === "1.4.1"
|
|
105
|
+
? SAFE_PROXY_CREATION_CODE
|
|
106
|
+
: SAFE_130_PROXY_CREATION_CODE;
|
|
107
|
+
}
|
|
17
108
|
const contracts = [
|
|
18
109
|
[
|
|
19
110
|
SAFE_FACTORY,
|
|
@@ -28,26 +119,145 @@ const contracts = [
|
|
|
28
119
|
"0x7c6007a5d711cea8dfd5d91f5940ec29c7f200fe511eb1fc1397b367af3c42f9",
|
|
29
120
|
],
|
|
30
121
|
];
|
|
31
|
-
const PROXY_CODE_HASH = "0xd7d408ebcd99b2b70be43e20253d6d92a8ea8fab29bd3be7f55b10032331fb4c";
|
|
32
122
|
const SENTINEL = "0x0000000000000000000000000000000000000001";
|
|
33
|
-
const SINGLETON_SLOT =
|
|
123
|
+
const SINGLETON_SLOT = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
34
124
|
const GUARD_SLOT = "0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8";
|
|
35
125
|
const FALLBACK_SLOT = "0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
]
|
|
126
|
+
const SETUP_FUNCTION = {
|
|
127
|
+
name: "setup",
|
|
128
|
+
type: "function",
|
|
129
|
+
stateMutability: "nonpayable",
|
|
130
|
+
inputs: [
|
|
131
|
+
{ type: "address[]", name: "owners" },
|
|
132
|
+
{ type: "uint256", name: "threshold" },
|
|
133
|
+
{ type: "address", name: "to" },
|
|
134
|
+
{ type: "bytes", name: "data" },
|
|
135
|
+
{ type: "address", name: "fallbackHandler" },
|
|
136
|
+
{ type: "address", name: "paymentToken" },
|
|
137
|
+
{ type: "uint256", name: "payment" },
|
|
138
|
+
{ type: "address", name: "paymentReceiver" },
|
|
139
|
+
],
|
|
140
|
+
outputs: [],
|
|
141
|
+
};
|
|
142
|
+
/** `Safe.setup`, the initializer a Safe proxy runs once at creation. */
|
|
143
|
+
export const SAFE_SETUP_ABI = [SETUP_FUNCTION];
|
|
144
|
+
/** `SafeToL2Setup.setupToL2`, the only delegatecall hook a replayed creation may run. */
|
|
145
|
+
export const SAFE_TO_L2_SETUP_ABI = [
|
|
146
|
+
{
|
|
147
|
+
name: "setupToL2",
|
|
148
|
+
type: "function",
|
|
149
|
+
stateMutability: "nonpayable",
|
|
150
|
+
inputs: [{ type: "address", name: "l2Singleton" }],
|
|
151
|
+
outputs: [],
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
export const SAFE_CREATE_ABI = [
|
|
155
|
+
{
|
|
156
|
+
name: "proxyCreationCode",
|
|
157
|
+
type: "function",
|
|
158
|
+
stateMutability: "pure",
|
|
159
|
+
inputs: [],
|
|
160
|
+
outputs: [{ type: "bytes" }],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "createProxyWithNonce",
|
|
164
|
+
type: "function",
|
|
165
|
+
stateMutability: "nonpayable",
|
|
166
|
+
inputs: [
|
|
167
|
+
{ type: "address", name: "singleton" },
|
|
168
|
+
{ type: "bytes", name: "initializer" },
|
|
169
|
+
{ type: "uint256", name: "saltNonce" },
|
|
170
|
+
],
|
|
171
|
+
outputs: [{ type: "address", name: "proxy" }],
|
|
172
|
+
},
|
|
173
|
+
SETUP_FUNCTION,
|
|
174
|
+
];
|
|
175
|
+
export const CREATE_BATCH_ABI = [
|
|
176
|
+
{
|
|
177
|
+
name: "aggregate3Value",
|
|
178
|
+
type: "function",
|
|
179
|
+
stateMutability: "payable",
|
|
180
|
+
inputs: [
|
|
181
|
+
{
|
|
182
|
+
type: "tuple[]",
|
|
183
|
+
name: "calls",
|
|
184
|
+
components: [
|
|
185
|
+
{ type: "address", name: "target" },
|
|
186
|
+
{ type: "bool", name: "allowFailure" },
|
|
187
|
+
{ type: "uint256", name: "value" },
|
|
188
|
+
{ type: "bytes", name: "callData" },
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
outputs: [
|
|
193
|
+
{
|
|
194
|
+
type: "tuple[]",
|
|
195
|
+
name: "returnData",
|
|
196
|
+
components: [
|
|
197
|
+
{ type: "bool", name: "success" },
|
|
198
|
+
{ type: "bytes", name: "returnData" },
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
const READ_ABI = [
|
|
205
|
+
{
|
|
206
|
+
name: "masterCopy",
|
|
207
|
+
type: "function",
|
|
208
|
+
stateMutability: "view",
|
|
209
|
+
inputs: [],
|
|
210
|
+
outputs: [{ type: "address" }],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "VERSION",
|
|
214
|
+
type: "function",
|
|
215
|
+
stateMutability: "view",
|
|
216
|
+
inputs: [],
|
|
217
|
+
outputs: [{ type: "string" }],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "getThreshold",
|
|
221
|
+
type: "function",
|
|
222
|
+
stateMutability: "view",
|
|
223
|
+
inputs: [],
|
|
224
|
+
outputs: [{ type: "uint256" }],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "getOwners",
|
|
228
|
+
type: "function",
|
|
229
|
+
stateMutability: "view",
|
|
230
|
+
inputs: [],
|
|
231
|
+
outputs: [{ type: "address[]" }],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "getModulesPaginated",
|
|
235
|
+
type: "function",
|
|
236
|
+
stateMutability: "view",
|
|
237
|
+
inputs: [
|
|
238
|
+
{ type: "address", name: "start" },
|
|
239
|
+
{ type: "uint256", name: "pageSize" },
|
|
240
|
+
],
|
|
241
|
+
outputs: [{ type: "address[]" }, { type: "address" }],
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: "nonce",
|
|
245
|
+
type: "function",
|
|
246
|
+
stateMutability: "view",
|
|
247
|
+
inputs: [],
|
|
248
|
+
outputs: [{ type: "uint256" }],
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "approvedHashes",
|
|
252
|
+
type: "function",
|
|
253
|
+
stateMutability: "view",
|
|
254
|
+
inputs: [
|
|
255
|
+
{ type: "address", name: "owner" },
|
|
256
|
+
{ type: "bytes32", name: "hash" },
|
|
257
|
+
],
|
|
258
|
+
outputs: [{ type: "uint256" }],
|
|
259
|
+
},
|
|
260
|
+
];
|
|
51
261
|
function validAddress(address) {
|
|
52
262
|
return isAddress(address) && BigInt(address) > 1n;
|
|
53
263
|
}
|
|
@@ -160,6 +370,11 @@ export async function resolveSafeAddress(input, clients) {
|
|
|
160
370
|
const plan = { ...policy, address: predictSafeAddress(policy) };
|
|
161
371
|
return { address: plan.address, plan };
|
|
162
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* One raw `eth_call` into a Safe, bounded by `gas` and `maxBytes` of return
|
|
375
|
+
* data: the result, or null when the node answered with anything but whole
|
|
376
|
+
* hex bytes within the bound. An RPC failure throws.
|
|
377
|
+
*/
|
|
163
378
|
async function boundedCall(client, address, data, gas, maxBytes, blockNumber) {
|
|
164
379
|
// Raw RPC cannot follow a contract-selected CCIP/OffchainLookup URL.
|
|
165
380
|
const result = await client.request({
|
|
@@ -169,12 +384,11 @@ async function boundedCall(client, address, data, gas, maxBytes, blockNumber) {
|
|
|
169
384
|
blockNumber === undefined ? "latest" : toHex(blockNumber),
|
|
170
385
|
],
|
|
171
386
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
result.length
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return result;
|
|
387
|
+
return typeof result === "string" &&
|
|
388
|
+
/^0x(?:[\da-f]{2})*$/i.test(result) &&
|
|
389
|
+
result.length <= 2 + maxBytes * 2
|
|
390
|
+
? result
|
|
391
|
+
: null;
|
|
178
392
|
}
|
|
179
393
|
/**
|
|
180
394
|
* Certify this module's plain Safe profile, failing closed on malformed state or RPC errors.
|
|
@@ -209,7 +423,13 @@ export async function verifySafeDeployments(client, plans, options = {}) {
|
|
|
209
423
|
if (!codeMatches(await client.getCode({ address, blockNumber }), hash))
|
|
210
424
|
throw mismatch();
|
|
211
425
|
}
|
|
212
|
-
const call = (data, maxBytes, gas = 100000n) =>
|
|
426
|
+
const call = async (data, maxBytes, gas = 100000n) => {
|
|
427
|
+
const result = await boundedCall(client, plan.address, data, gas, maxBytes, blockNumber);
|
|
428
|
+
if (result === null) {
|
|
429
|
+
throw new Error("Invalid or oversized Safe policy response.");
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
};
|
|
213
433
|
const [masterCopy, version, threshold, ownersData, modules, guard, fallback,] = await Promise.all([
|
|
214
434
|
call(encodeFunctionData({ abi: READ_ABI, functionName: "masterCopy" }), 32),
|
|
215
435
|
call(encodeFunctionData({ abi: READ_ABI, functionName: "VERSION" }), 96),
|
|
@@ -386,4 +606,825 @@ export async function verifySafeLaunchSimulation(client, plans = [], data) {
|
|
|
386
606
|
}
|
|
387
607
|
}
|
|
388
608
|
}
|
|
609
|
+
/** Gas each bounded Safe read may use: a word, the owner list, a module page. */
|
|
610
|
+
const SAFE_SCALAR_READ_GAS = 100000n;
|
|
611
|
+
const SAFE_OWNERS_READ_GAS = 400000n;
|
|
612
|
+
const SAFE_MODULES_READ_GAS = 500000n;
|
|
613
|
+
const SAFE_MODULE_PAGE = 64;
|
|
614
|
+
/** A `createProxyWithNonce` preflight's gas: a proxy, its setup and any hook. */
|
|
615
|
+
const SAFE_DEPLOY_SIMULATION_GAS = 3000000n;
|
|
616
|
+
function invalidArgument(name, value) {
|
|
617
|
+
return new Error(`Invalid ${name}: ${String(value)}.`);
|
|
618
|
+
}
|
|
619
|
+
/** The checksummed address, or a refusal naming `name`. */
|
|
620
|
+
function requireAddress(value, name) {
|
|
621
|
+
if (typeof value !== "string" || !isAddress(value)) {
|
|
622
|
+
throw invalidArgument(name, value);
|
|
623
|
+
}
|
|
624
|
+
return getAddress(value);
|
|
625
|
+
}
|
|
626
|
+
/** Byte-aligned code as eth_getCode returns it, with a lowercase 0x. */
|
|
627
|
+
function isRuntimeCode(code) {
|
|
628
|
+
return typeof code === "string" && /^0x(?:[\da-fA-F]{2})*$/.test(code);
|
|
629
|
+
}
|
|
630
|
+
/** The delegate of an exact 23-byte EIP-7702 designator, `0xef0100 ‖ address`. */
|
|
631
|
+
function eip7702Delegation(code) {
|
|
632
|
+
return typeof code === "string" && /^0x[eE][fF]0100[\da-fA-F]{40}$/.test(code)
|
|
633
|
+
? getAddress(`0x${code.slice(8).toLowerCase()}`)
|
|
634
|
+
: null;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Whether `code` is exactly an EIP-7702 delegation designator. A contract that
|
|
638
|
+
* only starts with the prefix is not, and stays a contract.
|
|
639
|
+
*/
|
|
640
|
+
export function isEip7702DelegatedEoaRuntime(code) {
|
|
641
|
+
return eip7702Delegation(code) !== null;
|
|
642
|
+
}
|
|
643
|
+
/** No code, or an exact EIP-7702 designator: the account's key still signs. */
|
|
644
|
+
function isEoaRuntime(code) {
|
|
645
|
+
return (code === undefined || code === "0x" || isEip7702DelegatedEoaRuntime(code));
|
|
646
|
+
}
|
|
647
|
+
/** The address in a 32-byte word whose upper 12 bytes are zero. */
|
|
648
|
+
function wordAddress(word) {
|
|
649
|
+
return typeof word === "string" && /^0x0{24}[\da-fA-F]{40}$/.test(word)
|
|
650
|
+
? getAddress(`0x${word.slice(26).toLowerCase()}`)
|
|
651
|
+
: null;
|
|
652
|
+
}
|
|
653
|
+
function safeReleaseOf(singleton) {
|
|
654
|
+
return RECOGNIZED_SAFE_RELEASES.find((release) => release.singletons.some((candidate) => isAddressEqual(candidate, singleton)));
|
|
655
|
+
}
|
|
656
|
+
/** Whether `factory` and `singleton` are one recognized Safe release. */
|
|
657
|
+
export function isRecognizedSafeDeployment(factory, singleton) {
|
|
658
|
+
return (isAddress(factory) &&
|
|
659
|
+
isAddress(singleton) &&
|
|
660
|
+
!!safeReleaseOf(singleton)?.factories.some((candidate) => isAddressEqual(candidate, factory)));
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Whether two singletons are one recognized release: the same address, or an
|
|
664
|
+
* Ethereum singleton and the SafeL2 singleton SafeToL2Setup pairs it with.
|
|
665
|
+
*/
|
|
666
|
+
export function safeSingletonsAreEquivalent(left, right) {
|
|
667
|
+
if (isAddressEqual(left, right))
|
|
668
|
+
return true;
|
|
669
|
+
return SAFE_L1_L2_SINGLETON_PAIRS.some(([l1, l2]) => (isAddressEqual(l1, left) && isAddressEqual(l2, right)) ||
|
|
670
|
+
(isAddressEqual(l1, right) && isAddressEqual(l2, left)));
|
|
671
|
+
}
|
|
672
|
+
/** One exact 32-byte word from a bounded read, or null. */
|
|
673
|
+
async function readSafeWord(client, safe, data, blockNumber) {
|
|
674
|
+
const result = await boundedCall(client, safe, data, SAFE_SCALAR_READ_GAS, 32, blockNumber);
|
|
675
|
+
return result?.length === 66 ? BigInt(result) : null;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* A Safe's nonce through one raw, gas- and return-bounded `eth_call`: null
|
|
679
|
+
* when the answer is not exactly one word. An RPC failure throws.
|
|
680
|
+
*/
|
|
681
|
+
export function readBoundedSafeNonce(client, safe, { blockNumber } = {}) {
|
|
682
|
+
return readSafeWord(client, requireAddress(safe, "Safe address"), encodeFunctionData({ abi: READ_ABI, functionName: "nonce" }), blockNumber);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* `approvedHashes(owner, hash)` through one raw, bounded `eth_call`: nonzero
|
|
686
|
+
* when `owner` approved the Safe transaction hash onchain. Null when the answer
|
|
687
|
+
* is not exactly one word. An RPC failure throws.
|
|
688
|
+
*/
|
|
689
|
+
export function readBoundedSafeApprovedHash(client, safe, owner, hash, { blockNumber } = {}) {
|
|
690
|
+
if (typeof hash !== "string" || !/^0x[\da-fA-F]{64}$/.test(hash)) {
|
|
691
|
+
throw invalidArgument("Safe transaction hash", hash);
|
|
692
|
+
}
|
|
693
|
+
return readSafeWord(client, requireAddress(safe, "Safe address"), encodeFunctionData({
|
|
694
|
+
abi: READ_ABI,
|
|
695
|
+
functionName: "approvedHashes",
|
|
696
|
+
args: [requireAddress(owner, "Safe owner"), hash],
|
|
697
|
+
}), blockNumber);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* `getOwners()`, bounded before anything is decoded: a crafted proxy's owner
|
|
701
|
+
* list could otherwise make the reader allocate without limit. Null unless the
|
|
702
|
+
* answer is exactly one canonical array of 1 to MAX_SAFE_OWNERS clean words.
|
|
703
|
+
*/
|
|
704
|
+
async function readSafeOwners(client, safe, blockNumber) {
|
|
705
|
+
const result = await boundedCall(client, safe, encodeFunctionData({ abi: READ_ABI, functionName: "getOwners" }), SAFE_OWNERS_READ_GAS, 64 + MAX_SAFE_OWNERS * 32, blockNumber);
|
|
706
|
+
if (!result || result.length < 130 || BigInt(result.slice(0, 66)) !== 32n) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
// The byte bound above already caps the count at MAX_SAFE_OWNERS.
|
|
710
|
+
const count = BigInt(`0x${result.slice(66, 130)}`);
|
|
711
|
+
if (count < 1n || result.length !== 130 + Number(count) * 64)
|
|
712
|
+
return null;
|
|
713
|
+
const owners = [];
|
|
714
|
+
for (let index = 0; index < Number(count); index += 1) {
|
|
715
|
+
const owner = wordAddress(`0x${result.slice(130 + index * 64, 194 + index * 64)}`);
|
|
716
|
+
if (!owner)
|
|
717
|
+
return null;
|
|
718
|
+
owners.push(owner);
|
|
719
|
+
}
|
|
720
|
+
return owners;
|
|
721
|
+
}
|
|
722
|
+
/** The first page of enabled modules, or null for anything but a canonical answer. */
|
|
723
|
+
async function readSafeModulePage(client, safe, blockNumber) {
|
|
724
|
+
const result = await boundedCall(client, safe, encodeFunctionData({
|
|
725
|
+
abi: READ_ABI,
|
|
726
|
+
functionName: "getModulesPaginated",
|
|
727
|
+
args: [SENTINEL, BigInt(SAFE_MODULE_PAGE)],
|
|
728
|
+
}), SAFE_MODULES_READ_GAS, 96 + SAFE_MODULE_PAGE * 32, blockNumber);
|
|
729
|
+
if (!result || result.length < 194 || BigInt(result.slice(0, 66)) !== 64n) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
const next = wordAddress(`0x${result.slice(66, 130)}`);
|
|
733
|
+
const count = BigInt(`0x${result.slice(130, 194)}`);
|
|
734
|
+
// The byte bound above already caps the count at one page.
|
|
735
|
+
if (!next || result.length !== 194 + Number(count) * 64)
|
|
736
|
+
return null;
|
|
737
|
+
const modules = [];
|
|
738
|
+
for (let index = 0; index < Number(count); index += 1) {
|
|
739
|
+
const module = wordAddress(`0x${result.slice(194 + index * 64, 258 + index * 64)}`);
|
|
740
|
+
if (!module || isAddressEqual(module, zeroAddress))
|
|
741
|
+
return null;
|
|
742
|
+
modules.push(module);
|
|
743
|
+
}
|
|
744
|
+
return { modules, next };
|
|
745
|
+
}
|
|
746
|
+
/** `VERSION()`, only when the answer is the canonical encoding of a string of at most 32 bytes. */
|
|
747
|
+
async function readSafeVersion(client, safe, blockNumber) {
|
|
748
|
+
const result = await boundedCall(client, safe, encodeFunctionData({ abi: READ_ABI, functionName: "VERSION" }), SAFE_SCALAR_READ_GAS, 96, blockNumber);
|
|
749
|
+
if (!result)
|
|
750
|
+
return null;
|
|
751
|
+
try {
|
|
752
|
+
const [version] = decodeAbiParameters([{ type: "string" }], result);
|
|
753
|
+
return encodeAbiParameters([{ type: "string" }], [version]) ===
|
|
754
|
+
result.toLowerCase()
|
|
755
|
+
? version
|
|
756
|
+
: null;
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Who controls `authority`, from live chain state alone:
|
|
764
|
+
*
|
|
765
|
+
* - no code is an `eoa`; an exact EIP-7702 designator a `delegated-eoa`;
|
|
766
|
+
* - a `safe` is a recognized proxy runtime whose slot zero names a recognized
|
|
767
|
+
* singleton with code, agreeing with `masterCopy()`, reporting that
|
|
768
|
+
* release's exact version, with a well-formed policy: 1 to 50 unique nonzero
|
|
769
|
+
* owners, a threshold within them, clean guard and fallback handler slots,
|
|
770
|
+
* and a fallback handler that is contract code (not a delegated EOA);
|
|
771
|
+
* - anything else, or any malformed answer, is a `contract`.
|
|
772
|
+
*
|
|
773
|
+
* Nothing calls through the proxy before its singleton is known, and every
|
|
774
|
+
* such call is a raw, gas- and return-bounded `eth_call`, so a contract cannot
|
|
775
|
+
* pick a URL to fetch. An RPC failure is null (unknown), never an EOA or Safe.
|
|
776
|
+
*/
|
|
777
|
+
export async function readAuthorityIdentity(client, authority, { blockNumber } = {}) {
|
|
778
|
+
const address = requireAddress(authority, "authority address");
|
|
779
|
+
let code;
|
|
780
|
+
try {
|
|
781
|
+
code = await client.getCode({ address, blockNumber });
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
if (code === undefined || code === "0x")
|
|
787
|
+
return { kind: "eoa" };
|
|
788
|
+
if (!isRuntimeCode(code))
|
|
789
|
+
return { kind: "contract" };
|
|
790
|
+
const delegation = eip7702Delegation(code);
|
|
791
|
+
if (delegation)
|
|
792
|
+
return { kind: "delegated-eoa", delegation };
|
|
793
|
+
const proxyCodeHash = keccak256(code);
|
|
794
|
+
if (!RECOGNIZED_SAFE_PROXY_CODE_HASHES.some((hash) => hash === proxyCodeHash)) {
|
|
795
|
+
return { kind: "contract" };
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
const singleton = wordAddress(await client.getStorageAt({ address, slot: SINGLETON_SLOT, blockNumber }));
|
|
799
|
+
const release = singleton && safeReleaseOf(singleton);
|
|
800
|
+
if (!singleton || !release)
|
|
801
|
+
return { kind: "contract" };
|
|
802
|
+
const singletonCode = await client.getCode({
|
|
803
|
+
address: singleton,
|
|
804
|
+
blockNumber,
|
|
805
|
+
});
|
|
806
|
+
if (!isRuntimeCode(singletonCode) || singletonCode === "0x") {
|
|
807
|
+
return { kind: "contract" };
|
|
808
|
+
}
|
|
809
|
+
const masterCopy = await boundedCall(client, address, encodeFunctionData({ abi: READ_ABI, functionName: "masterCopy" }), SAFE_SCALAR_READ_GAS, 32, blockNumber);
|
|
810
|
+
const reported = wordAddress(masterCopy);
|
|
811
|
+
if (!reported || !isAddressEqual(reported, singleton)) {
|
|
812
|
+
return { kind: "contract" };
|
|
813
|
+
}
|
|
814
|
+
const [threshold, owners, modulePage, version, guardWord, fallbackWord] = await Promise.all([
|
|
815
|
+
readSafeWord(client, address, encodeFunctionData({ abi: READ_ABI, functionName: "getThreshold" }), blockNumber),
|
|
816
|
+
readSafeOwners(client, address, blockNumber),
|
|
817
|
+
readSafeModulePage(client, address, blockNumber),
|
|
818
|
+
readSafeVersion(client, address, blockNumber),
|
|
819
|
+
client.getStorageAt({ address, slot: GUARD_SLOT, blockNumber }),
|
|
820
|
+
client.getStorageAt({ address, slot: FALLBACK_SLOT, blockNumber }),
|
|
821
|
+
]);
|
|
822
|
+
const guard = wordAddress(guardWord);
|
|
823
|
+
const fallbackHandler = wordAddress(fallbackWord);
|
|
824
|
+
if (!owners ||
|
|
825
|
+
!ownerSet(owners) ||
|
|
826
|
+
threshold === null ||
|
|
827
|
+
threshold < 1n ||
|
|
828
|
+
threshold > BigInt(owners.length) ||
|
|
829
|
+
!modulePage ||
|
|
830
|
+
version !== release.version ||
|
|
831
|
+
!guard ||
|
|
832
|
+
!fallbackHandler) {
|
|
833
|
+
return { kind: "contract" };
|
|
834
|
+
}
|
|
835
|
+
const hasFallback = !isAddressEqual(fallbackHandler, zeroAddress);
|
|
836
|
+
const codes = await Promise.all([...owners, ...(hasFallback ? [fallbackHandler] : [])].map((target) => client.getCode({ address: target, blockNumber })));
|
|
837
|
+
const fallbackCode = hasFallback ? codes[owners.length] : undefined;
|
|
838
|
+
if (hasFallback &&
|
|
839
|
+
(!isRuntimeCode(fallbackCode) ||
|
|
840
|
+
fallbackCode === "0x" ||
|
|
841
|
+
isEip7702DelegatedEoaRuntime(fallbackCode))) {
|
|
842
|
+
// A fallback handler runs as code. A delegated EOA's runtime follows its
|
|
843
|
+
// delegate, which can differ by chain behind the same 23-byte marker.
|
|
844
|
+
return { kind: "contract" };
|
|
845
|
+
}
|
|
846
|
+
const { modules, next } = modulePage;
|
|
847
|
+
const lastPage = isAddressEqual(next, SENTINEL);
|
|
848
|
+
return {
|
|
849
|
+
kind: "safe",
|
|
850
|
+
owners,
|
|
851
|
+
threshold: Number(threshold),
|
|
852
|
+
ownersAreEoas: codes.slice(0, owners.length).every(isEoaRuntime),
|
|
853
|
+
hasModules: modules.length > 0 || !lastPage,
|
|
854
|
+
modules: lastPage ? modules : null,
|
|
855
|
+
proxyCodeHash,
|
|
856
|
+
singleton,
|
|
857
|
+
singletonCodeHash: keccak256(singletonCode),
|
|
858
|
+
version: release.version,
|
|
859
|
+
guard,
|
|
860
|
+
fallbackHandler,
|
|
861
|
+
fallbackHandlerCodeHash: fallbackCode ? keccak256(fallbackCode) : null,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/** Sorted lowercase owners: 1 to MAX_SAFE_OWNERS unique, valid, nonzero addresses. */
|
|
869
|
+
function ownerSet(owners) {
|
|
870
|
+
if (!Array.isArray(owners) ||
|
|
871
|
+
owners.length < 1 ||
|
|
872
|
+
owners.length > MAX_SAFE_OWNERS) {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
const normalized = [];
|
|
876
|
+
for (const owner of owners) {
|
|
877
|
+
if (typeof owner !== "string" ||
|
|
878
|
+
!isAddress(owner) ||
|
|
879
|
+
isAddressEqual(owner, zeroAddress)) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
normalized.push(owner.toLowerCase());
|
|
883
|
+
}
|
|
884
|
+
const unique = [...new Set(normalized)].sort();
|
|
885
|
+
return unique.length === normalized.length ? unique : null;
|
|
886
|
+
}
|
|
887
|
+
function sameOwners(left, right) {
|
|
888
|
+
const leftSet = ownerSet(left);
|
|
889
|
+
const rightSet = ownerSet(right);
|
|
890
|
+
return (!!leftSet &&
|
|
891
|
+
!!rightSet &&
|
|
892
|
+
leftSet.length === rightSet.length &&
|
|
893
|
+
leftSet.every((owner, index) => owner === rightSet[index]));
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* The plain Safe policy a cross-chain claim or a same-address replay can
|
|
897
|
+
* stand on: EOA owners, no modules, no guard.
|
|
898
|
+
*/
|
|
899
|
+
export function isDeployableSafeAuthority(identity) {
|
|
900
|
+
return (identity.kind === "safe" &&
|
|
901
|
+
identity.ownersAreEoas &&
|
|
902
|
+
!identity.hasModules &&
|
|
903
|
+
isAddressEqual(identity.guard, zeroAddress));
|
|
904
|
+
}
|
|
905
|
+
function isEoaIdentity(identity) {
|
|
906
|
+
return identity.kind === "eoa" || identity.kind === "delegated-eoa";
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Whether one authority shows the same policy on two chains: an EOA (plain or
|
|
910
|
+
* delegated, the key is the same) on both, or plain Safes with the same owners,
|
|
911
|
+
* threshold, proxy, release, fallback handler and code. Paired Ethereum and
|
|
912
|
+
* SafeL2 singletons are one release; a shared singleton address must also
|
|
913
|
+
* share its code.
|
|
914
|
+
*
|
|
915
|
+
* For Safes this is the visible policy, not proof of control. A setup hook can
|
|
916
|
+
* plant an owner or module no getter shows, and a Safe made with CREATE can be
|
|
917
|
+
* claimed at the same address on another chain. Pair it with
|
|
918
|
+
* {@link proveSafeCreation}, as {@link readMatchingAuthorityIdentities} and
|
|
919
|
+
* {@link readCrossChainHandleAuthority} do.
|
|
920
|
+
*/
|
|
921
|
+
export function authorityIdentitiesMatch(source, destination) {
|
|
922
|
+
if (isEoaIdentity(source) || isEoaIdentity(destination)) {
|
|
923
|
+
return isEoaIdentity(source) && isEoaIdentity(destination);
|
|
924
|
+
}
|
|
925
|
+
return (isDeployableSafeAuthority(source) &&
|
|
926
|
+
isDeployableSafeAuthority(destination) &&
|
|
927
|
+
sameOwners(source.owners, destination.owners) &&
|
|
928
|
+
source.threshold === destination.threshold &&
|
|
929
|
+
source.proxyCodeHash.toLowerCase() ===
|
|
930
|
+
destination.proxyCodeHash.toLowerCase() &&
|
|
931
|
+
safeSingletonsAreEquivalent(source.singleton, destination.singleton) &&
|
|
932
|
+
(!isAddressEqual(source.singleton, destination.singleton) ||
|
|
933
|
+
source.singletonCodeHash.toLowerCase() ===
|
|
934
|
+
destination.singletonCodeHash.toLowerCase()) &&
|
|
935
|
+
source.version === destination.version &&
|
|
936
|
+
isAddressEqual(source.fallbackHandler, destination.fallbackHandler) &&
|
|
937
|
+
source.fallbackHandlerCodeHash?.toLowerCase() ===
|
|
938
|
+
destination.fallbackHandlerCodeHash?.toLowerCase());
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Both chains' identities and whether one authority controls `authority` on
|
|
942
|
+
* both, or null when either is unknown. EOAs match on their key. Safes match
|
|
943
|
+
* only on the same visible policy ({@link authorityIdentitiesMatch}) and a
|
|
944
|
+
* `creation` that proves how the Safe was made ({@link proveSafeCreation});
|
|
945
|
+
* without one, `matches` is false and `creationUnproven` is true.
|
|
946
|
+
*/
|
|
947
|
+
export async function readMatchingAuthorityIdentities({ sourceClient, destinationClient, authority, sourceBlockNumber, destinationBlockNumber, creation, }) {
|
|
948
|
+
const [source, destination] = await Promise.all([
|
|
949
|
+
readAuthorityIdentity(sourceClient, authority, {
|
|
950
|
+
blockNumber: sourceBlockNumber,
|
|
951
|
+
}),
|
|
952
|
+
readAuthorityIdentity(destinationClient, authority, {
|
|
953
|
+
blockNumber: destinationBlockNumber,
|
|
954
|
+
}),
|
|
955
|
+
]);
|
|
956
|
+
if (!source || !destination)
|
|
957
|
+
return null;
|
|
958
|
+
const visible = authorityIdentitiesMatch(source, destination);
|
|
959
|
+
const creationUnproven = visible &&
|
|
960
|
+
source.kind === "safe" &&
|
|
961
|
+
!(creation && proveSafeCreation(creation, authority).valid);
|
|
962
|
+
return {
|
|
963
|
+
source,
|
|
964
|
+
destination,
|
|
965
|
+
matches: visible && !creationUnproven,
|
|
966
|
+
creationUnproven,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function handleVerdict(status, source, mainnet) {
|
|
970
|
+
return {
|
|
971
|
+
status,
|
|
972
|
+
allowed: status === "valid-local" ||
|
|
973
|
+
status === "valid-eoa" ||
|
|
974
|
+
status === "valid-safe",
|
|
975
|
+
source,
|
|
976
|
+
mainnet,
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
async function addressesAreEoas(client, addresses, blockNumber) {
|
|
980
|
+
try {
|
|
981
|
+
const codes = await Promise.all(addresses.map((address) => client.getCode({ address, blockNumber })));
|
|
982
|
+
return codes.every(isEoaRuntime);
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Whether `authority`, the live owner or operator of a project on
|
|
990
|
+
* `sourceChainId`, may publish its Ethereum handle. On Ethereum it may. From
|
|
991
|
+
* another chain it must be the same EOA on Ethereum, or a plain Safe there with
|
|
992
|
+
* the same policy whose `creation` proves how it was made
|
|
993
|
+
* ({@link proveSafeCreation}); without that proof it is `unproven-creation`.
|
|
994
|
+
* A source Safe not yet deployed on Ethereum is `missing-mainnet-safe`, which a
|
|
995
|
+
* same-address deployment can fix; every other doubt is denied, an unreadable
|
|
996
|
+
* chain as `unknown`.
|
|
997
|
+
*/
|
|
998
|
+
export async function readCrossChainHandleAuthority({ sourceChainId, sourceClient, mainnetClient, authority, sourceBlockNumber, mainnetBlockNumber, creation, }) {
|
|
999
|
+
if (sourceChainId === 1)
|
|
1000
|
+
return handleVerdict("valid-local", null, null);
|
|
1001
|
+
if (!mainnetClient)
|
|
1002
|
+
return handleVerdict("unknown", null, null);
|
|
1003
|
+
const [source, mainnet] = await Promise.all([
|
|
1004
|
+
readAuthorityIdentity(sourceClient, authority, {
|
|
1005
|
+
blockNumber: sourceBlockNumber,
|
|
1006
|
+
}),
|
|
1007
|
+
readAuthorityIdentity(mainnetClient, authority, {
|
|
1008
|
+
blockNumber: mainnetBlockNumber,
|
|
1009
|
+
}),
|
|
1010
|
+
]);
|
|
1011
|
+
const verdict = (status) => handleVerdict(status, source, mainnet);
|
|
1012
|
+
if (!source || !mainnet)
|
|
1013
|
+
return verdict("unknown");
|
|
1014
|
+
if (source.kind === "contract")
|
|
1015
|
+
return verdict("source-contract");
|
|
1016
|
+
if (source.kind !== "safe") {
|
|
1017
|
+
if (isEoaIdentity(mainnet))
|
|
1018
|
+
return verdict("valid-eoa");
|
|
1019
|
+
return verdict(mainnet.kind === "contract" ? "mainnet-contract" : "authority-mismatch");
|
|
1020
|
+
}
|
|
1021
|
+
if (!source.ownersAreEoas)
|
|
1022
|
+
return verdict("contract-owner");
|
|
1023
|
+
if (!isDeployableSafeAuthority(source))
|
|
1024
|
+
return verdict("unsafe-safe-policy");
|
|
1025
|
+
if (mainnet.kind === "delegated-eoa") {
|
|
1026
|
+
// The designator occupies the address, so no Safe can be deployed there.
|
|
1027
|
+
return verdict("authority-mismatch");
|
|
1028
|
+
}
|
|
1029
|
+
if (mainnet.kind === "eoa") {
|
|
1030
|
+
const ownersAreEoas = await addressesAreEoas(mainnetClient, source.owners, mainnetBlockNumber);
|
|
1031
|
+
if (ownersAreEoas === null)
|
|
1032
|
+
return verdict("unknown");
|
|
1033
|
+
return verdict(ownersAreEoas ? "missing-mainnet-safe" : "contract-owner");
|
|
1034
|
+
}
|
|
1035
|
+
if (mainnet.kind === "contract")
|
|
1036
|
+
return verdict("mainnet-contract");
|
|
1037
|
+
if (!mainnet.ownersAreEoas)
|
|
1038
|
+
return verdict("contract-owner");
|
|
1039
|
+
if (!isDeployableSafeAuthority(mainnet)) {
|
|
1040
|
+
return verdict("unsafe-safe-policy");
|
|
1041
|
+
}
|
|
1042
|
+
if (!authorityIdentitiesMatch(source, mainnet)) {
|
|
1043
|
+
return verdict("authority-mismatch");
|
|
1044
|
+
}
|
|
1045
|
+
return verdict(creation && proveSafeCreation(creation, authority).valid
|
|
1046
|
+
? "valid-safe"
|
|
1047
|
+
: "unproven-creation");
|
|
1048
|
+
}
|
|
1049
|
+
/** The exact `setupToL2(l2Singleton)` naming `singleton`'s SafeL2 pair. */
|
|
1050
|
+
function isExactSetupToL2Call(data, singleton) {
|
|
1051
|
+
const pair = SAFE_L1_L2_SINGLETON_PAIRS.find(([l1]) => isAddressEqual(l1, singleton));
|
|
1052
|
+
return (!!pair &&
|
|
1053
|
+
data.toLowerCase() ===
|
|
1054
|
+
encodeFunctionData({
|
|
1055
|
+
abi: SAFE_TO_L2_SETUP_ABI,
|
|
1056
|
+
functionName: "setupToL2",
|
|
1057
|
+
args: [pair[1]],
|
|
1058
|
+
}).toLowerCase());
|
|
1059
|
+
}
|
|
1060
|
+
/** Why `creation` is malformed or not one recognized Safe release, or null. */
|
|
1061
|
+
function safeCreationShapeRefusal(creation) {
|
|
1062
|
+
if (!creation ||
|
|
1063
|
+
typeof creation.factory !== "string" ||
|
|
1064
|
+
typeof creation.singleton !== "string" ||
|
|
1065
|
+
!isAddress(creation.factory) ||
|
|
1066
|
+
!isAddress(creation.singleton) ||
|
|
1067
|
+
!isHexBytes(creation.initializer) ||
|
|
1068
|
+
uint256(creation.saltNonce) === null ||
|
|
1069
|
+
typeof creation.saltNonce !== "bigint") {
|
|
1070
|
+
return "malformed-creation";
|
|
1071
|
+
}
|
|
1072
|
+
if (!isRecognizedSafeDeployment(creation.factory, creation.singleton)) {
|
|
1073
|
+
return "unrecognized-deployment";
|
|
1074
|
+
}
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* `setup`'s arguments when `initializer` is exactly their canonical encoding,
|
|
1079
|
+
* with a valid owner set and threshold; null otherwise. A noncanonical
|
|
1080
|
+
* encoding, trailing bytes included, could hide a policy.
|
|
1081
|
+
*/
|
|
1082
|
+
function canonicalSafeSetupArgs(initializer) {
|
|
1083
|
+
let args;
|
|
1084
|
+
try {
|
|
1085
|
+
args = decodeFunctionData({ abi: SAFE_SETUP_ABI, data: initializer }).args;
|
|
1086
|
+
if (encodeFunctionData({
|
|
1087
|
+
abi: SAFE_SETUP_ABI,
|
|
1088
|
+
functionName: "setup",
|
|
1089
|
+
args,
|
|
1090
|
+
}).toLowerCase() !== initializer.toLowerCase()) {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
const [owners, threshold] = args;
|
|
1098
|
+
if (!ownerSet(owners) ||
|
|
1099
|
+
threshold < 1n ||
|
|
1100
|
+
threshold > BigInt(owners.length)) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
return args;
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Whether `setup` runs no delegatecall hook but the exact SafeToL2Setup call
|
|
1107
|
+
* pairing `singleton`, and pays no one.
|
|
1108
|
+
*/
|
|
1109
|
+
function safeSetupIsInert(args, singleton) {
|
|
1110
|
+
const [, , to, data, , paymentToken, payment, paymentReceiver] = args;
|
|
1111
|
+
const hook = isAddressEqual(to, SAFE_TO_L2_SETUP_ADDRESS)
|
|
1112
|
+
? isExactSetupToL2Call(data, singleton)
|
|
1113
|
+
: isAddressEqual(to, zeroAddress) && data === "0x";
|
|
1114
|
+
return (hook &&
|
|
1115
|
+
isAddressEqual(paymentToken, zeroAddress) &&
|
|
1116
|
+
payment === 0n &&
|
|
1117
|
+
(isAddressEqual(paymentReceiver, zeroAddress) ||
|
|
1118
|
+
isAddressEqual(paymentReceiver, SAFE_CANONICAL_PAYMENT_RECEIVER)));
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Whether replaying `creation` reproduces `current`, the Safe's live policy,
|
|
1122
|
+
* and nothing else: a recognized factory and singleton of its release, a
|
|
1123
|
+
* canonical `setup` with today's owners, threshold and fallback handler, no
|
|
1124
|
+
* setup payment, and no delegatecall hook but the exact SafeToL2Setup call.
|
|
1125
|
+
* The live Safe must be plain (EOA owners, no modules, no guard).
|
|
1126
|
+
*/
|
|
1127
|
+
export function validateSafeCreationForCurrentPolicy(creation, current) {
|
|
1128
|
+
const refuse = (reason) => ({ valid: false, reason });
|
|
1129
|
+
const shape = safeCreationShapeRefusal(creation);
|
|
1130
|
+
if (shape)
|
|
1131
|
+
return refuse(shape);
|
|
1132
|
+
if (!isDeployableSafeAuthority(current) ||
|
|
1133
|
+
!RECOGNIZED_SAFE_PROXY_CODE_HASHES.some((hash) => hash === current.proxyCodeHash.toLowerCase()) ||
|
|
1134
|
+
safeReleaseOf(current.singleton)?.version !== current.version ||
|
|
1135
|
+
(isAddressEqual(current.fallbackHandler, zeroAddress)
|
|
1136
|
+
? current.fallbackHandlerCodeHash !== null
|
|
1137
|
+
: current.fallbackHandlerCodeHash === null)) {
|
|
1138
|
+
return refuse("unsafe-current-policy");
|
|
1139
|
+
}
|
|
1140
|
+
if (!safeSingletonsAreEquivalent(creation.singleton, current.singleton)) {
|
|
1141
|
+
return refuse("initializer-policy-mismatch");
|
|
1142
|
+
}
|
|
1143
|
+
const args = canonicalSafeSetupArgs(creation.initializer);
|
|
1144
|
+
if (!args)
|
|
1145
|
+
return refuse("malformed-initializer");
|
|
1146
|
+
const [owners, thresholdRaw, , , fallbackHandler] = args;
|
|
1147
|
+
if (thresholdRaw !== BigInt(current.threshold) ||
|
|
1148
|
+
!sameOwners(owners, current.owners) ||
|
|
1149
|
+
!isAddressEqual(fallbackHandler, current.fallbackHandler)) {
|
|
1150
|
+
return refuse("initializer-policy-mismatch");
|
|
1151
|
+
}
|
|
1152
|
+
if (!safeSetupIsInert(args, creation.singleton)) {
|
|
1153
|
+
return refuse("unsafe-initializer");
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
valid: true,
|
|
1157
|
+
owners: owners.map((owner) => getAddress(owner)),
|
|
1158
|
+
threshold: Number(thresholdRaw),
|
|
1159
|
+
fallbackHandler: getAddress(fallbackHandler),
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Whether `creation` is how `safe` was created, so that no one but its owners
|
|
1164
|
+
* can hold the same address on another chain:
|
|
1165
|
+
*
|
|
1166
|
+
* - a recognized release's factory and singleton;
|
|
1167
|
+
* - the exact canonical `setup`, with no delegatecall hook but the exact
|
|
1168
|
+
* SafeToL2Setup call and no payment, so it planted no owner or module that
|
|
1169
|
+
* `getOwners` or `getModulesPaginated` cannot show;
|
|
1170
|
+
* - its CREATE2 address (the factory, keccak256(keccak256(initializer) ‖
|
|
1171
|
+
* saltNonce), and the release's proxy creation code with the singleton) is
|
|
1172
|
+
* `safe`.
|
|
1173
|
+
*
|
|
1174
|
+
* The address then pins those inputs on every chain where the factory has its
|
|
1175
|
+
* canonical code. A Safe made with CREATE (1.3.0's `createProxy`) has no such
|
|
1176
|
+
* proof: its address depends only on the factory's nonce, which anyone can take
|
|
1177
|
+
* on another chain. Read `creation` with `fetchSafeCreation` from the chain
|
|
1178
|
+
* where the Safe was made.
|
|
1179
|
+
*/
|
|
1180
|
+
export function proveSafeCreation(creation, safe) {
|
|
1181
|
+
const refuse = (reason) => ({ valid: false, reason });
|
|
1182
|
+
if (typeof safe !== "string" || !isAddress(safe)) {
|
|
1183
|
+
return refuse("malformed-creation");
|
|
1184
|
+
}
|
|
1185
|
+
const shape = safeCreationShapeRefusal(creation);
|
|
1186
|
+
if (shape)
|
|
1187
|
+
return refuse(shape);
|
|
1188
|
+
const args = canonicalSafeSetupArgs(creation.initializer);
|
|
1189
|
+
if (!args)
|
|
1190
|
+
return refuse("malformed-initializer");
|
|
1191
|
+
if (!safeSetupIsInert(args, creation.singleton)) {
|
|
1192
|
+
return refuse("unsafe-initializer");
|
|
1193
|
+
}
|
|
1194
|
+
const created = getContractAddress({
|
|
1195
|
+
opcode: "CREATE2",
|
|
1196
|
+
from: getAddress(creation.factory),
|
|
1197
|
+
salt: keccak256(encodePacked(["bytes32", "uint256"], [keccak256(creation.initializer), creation.saltNonce])),
|
|
1198
|
+
bytecode: concatHex([
|
|
1199
|
+
proxyCreationCodeOf(creation.factory),
|
|
1200
|
+
toHex(BigInt(creation.singleton), { size: 32 }),
|
|
1201
|
+
]),
|
|
1202
|
+
});
|
|
1203
|
+
if (!isAddressEqual(created, safe))
|
|
1204
|
+
return refuse("address-mismatch");
|
|
1205
|
+
const [owners, threshold, , , fallbackHandler] = args;
|
|
1206
|
+
return {
|
|
1207
|
+
valid: true,
|
|
1208
|
+
owners: owners.map((owner) => getAddress(owner)),
|
|
1209
|
+
threshold: Number(threshold),
|
|
1210
|
+
fallbackHandler: getAddress(fallbackHandler),
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
/** The exact factory call that reproduces a Safe's CREATE2 address. */
|
|
1214
|
+
export function buildSafeProxyFactoryCall(creation) {
|
|
1215
|
+
if (!isRecognizedSafeDeployment(creation.factory, creation.singleton)) {
|
|
1216
|
+
throw new Error(`Factory ${String(creation.factory)} and singleton ${String(creation.singleton)} are not one recognized Safe release.`);
|
|
1217
|
+
}
|
|
1218
|
+
const args = [
|
|
1219
|
+
getAddress(creation.singleton),
|
|
1220
|
+
creation.initializer,
|
|
1221
|
+
creation.saltNonce,
|
|
1222
|
+
];
|
|
1223
|
+
return {
|
|
1224
|
+
target: getAddress(creation.factory),
|
|
1225
|
+
abi: SAFE_CREATE_ABI,
|
|
1226
|
+
functionName: "createProxyWithNonce",
|
|
1227
|
+
args,
|
|
1228
|
+
data: encodeFunctionData({
|
|
1229
|
+
abi: SAFE_CREATE_ABI,
|
|
1230
|
+
functionName: "createProxyWithNonce",
|
|
1231
|
+
args,
|
|
1232
|
+
}),
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Prove, before anything is signed, that deploying `creation` on the
|
|
1237
|
+
* destination chain puts the source Safe's live policy at `safe`:
|
|
1238
|
+
*
|
|
1239
|
+
* - the source Safe is plain and `creation` reproduces it exactly
|
|
1240
|
+
* ({@link validateSafeCreationForCurrentPolicy});
|
|
1241
|
+
* - `safe` has no code on the destination, an EIP-7702 designator included;
|
|
1242
|
+
* - the factory and singleton there have the source chain's code, the
|
|
1243
|
+
* singleton the source Safe's own when it is the same address, and a
|
|
1244
|
+
* SafeToL2Setup hook the canonical library code;
|
|
1245
|
+
* - every owner is an EOA there, and a fallback handler is the same contract
|
|
1246
|
+
* code, not a delegated EOA;
|
|
1247
|
+
* - a raw, bounded `eth_call` of the factory call returns `safe`.
|
|
1248
|
+
*
|
|
1249
|
+
* Re-run it before the wallet signs, and read both chains' identities again
|
|
1250
|
+
* after the receipt ({@link readMatchingAuthorityIdentities}).
|
|
1251
|
+
*/
|
|
1252
|
+
export async function prepareSafeSameAddressDeployment({ sourceClient, destinationClient, creation, safe, from, }) {
|
|
1253
|
+
const expected = requireAddress(safe, "Safe address");
|
|
1254
|
+
const sender = requireAddress(from, "deployment sender");
|
|
1255
|
+
const refuse = (reason) => ({ valid: false, reason });
|
|
1256
|
+
const [source, destination] = await Promise.all([
|
|
1257
|
+
readAuthorityIdentity(sourceClient, expected),
|
|
1258
|
+
readAuthorityIdentity(destinationClient, expected),
|
|
1259
|
+
]);
|
|
1260
|
+
if (!source || !destination)
|
|
1261
|
+
return refuse("rpc-error");
|
|
1262
|
+
if (source.kind !== "safe")
|
|
1263
|
+
return refuse("not-a-safe");
|
|
1264
|
+
const validation = validateSafeCreationForCurrentPolicy(creation, source);
|
|
1265
|
+
if (!validation.valid)
|
|
1266
|
+
return refuse(validation.reason);
|
|
1267
|
+
if (destination.kind !== "eoa")
|
|
1268
|
+
return refuse("address-occupied");
|
|
1269
|
+
// The validation above decoded this exact initializer.
|
|
1270
|
+
const usesHook = isAddressEqual(decodeFunctionData({ abi: SAFE_SETUP_ABI, data: creation.initializer })
|
|
1271
|
+
.args[2], SAFE_TO_L2_SETUP_ADDRESS);
|
|
1272
|
+
const hasFallback = !isAddressEqual(source.fallbackHandler, zeroAddress);
|
|
1273
|
+
let codes;
|
|
1274
|
+
let sourceCodes;
|
|
1275
|
+
try {
|
|
1276
|
+
[codes, sourceCodes] = await Promise.all([
|
|
1277
|
+
Promise.all([
|
|
1278
|
+
creation.factory,
|
|
1279
|
+
creation.singleton,
|
|
1280
|
+
SAFE_TO_L2_SETUP_ADDRESS,
|
|
1281
|
+
...(hasFallback ? [source.fallbackHandler] : []),
|
|
1282
|
+
...source.owners,
|
|
1283
|
+
].map((address) => destinationClient.getCode({ address }))),
|
|
1284
|
+
Promise.all([creation.factory, creation.singleton].map((address) => sourceClient.getCode({ address }))),
|
|
1285
|
+
]);
|
|
1286
|
+
}
|
|
1287
|
+
catch {
|
|
1288
|
+
return refuse("rpc-error");
|
|
1289
|
+
}
|
|
1290
|
+
if ([...codes, ...sourceCodes].some((code) => code !== undefined && !isRuntimeCode(code))) {
|
|
1291
|
+
return refuse("rpc-error");
|
|
1292
|
+
}
|
|
1293
|
+
const [factoryCode, singletonCode, hookCode] = codes;
|
|
1294
|
+
const fallbackCode = hasFallback ? codes[3] : undefined;
|
|
1295
|
+
const ownerCodes = codes.slice(hasFallback ? 4 : 3);
|
|
1296
|
+
const [sourceFactoryCode, sourceSingletonCode] = sourceCodes;
|
|
1297
|
+
if (!factoryCode || factoryCode === "0x")
|
|
1298
|
+
return refuse("factory-unavailable");
|
|
1299
|
+
if (factoryCode.toLowerCase() !== sourceFactoryCode?.toLowerCase()) {
|
|
1300
|
+
return refuse("factory-mismatch");
|
|
1301
|
+
}
|
|
1302
|
+
if (!singletonCode || singletonCode === "0x") {
|
|
1303
|
+
return refuse("singleton-unavailable");
|
|
1304
|
+
}
|
|
1305
|
+
if (singletonCode.toLowerCase() !== sourceSingletonCode?.toLowerCase() ||
|
|
1306
|
+
(isAddressEqual(creation.singleton, source.singleton) &&
|
|
1307
|
+
keccak256(singletonCode) !== source.singletonCodeHash.toLowerCase())) {
|
|
1308
|
+
return refuse("singleton-mismatch");
|
|
1309
|
+
}
|
|
1310
|
+
if (usesHook &&
|
|
1311
|
+
(!hookCode ||
|
|
1312
|
+
hookCode === "0x" ||
|
|
1313
|
+
keccak256(hookCode) !== SAFE_TO_L2_SETUP_CODE_HASH)) {
|
|
1314
|
+
return refuse("setup-library-mismatch");
|
|
1315
|
+
}
|
|
1316
|
+
if (!ownerCodes.every(isEoaRuntime))
|
|
1317
|
+
return refuse("contract-owner");
|
|
1318
|
+
if (hasFallback) {
|
|
1319
|
+
if (!fallbackCode || fallbackCode === "0x") {
|
|
1320
|
+
return refuse("fallback-handler-unavailable");
|
|
1321
|
+
}
|
|
1322
|
+
if (isEip7702DelegatedEoaRuntime(fallbackCode)) {
|
|
1323
|
+
return refuse("delegated-fallback-handler");
|
|
1324
|
+
}
|
|
1325
|
+
if (keccak256(fallbackCode) !== source.fallbackHandlerCodeHash?.toLowerCase()) {
|
|
1326
|
+
return refuse("fallback-handler-mismatch");
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const call = buildSafeProxyFactoryCall(creation);
|
|
1330
|
+
let result;
|
|
1331
|
+
try {
|
|
1332
|
+
result = await simulateStateChangingTransaction(destinationClient, {
|
|
1333
|
+
from: sender,
|
|
1334
|
+
to: call.target,
|
|
1335
|
+
data: call.data,
|
|
1336
|
+
gas: SAFE_DEPLOY_SIMULATION_GAS,
|
|
1337
|
+
maxReturnBytes: 32,
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
catch {
|
|
1341
|
+
return refuse("simulation-failed");
|
|
1342
|
+
}
|
|
1343
|
+
const deployed = result.length === 66 ? wordAddress(result) : null;
|
|
1344
|
+
if (!deployed || !isAddressEqual(deployed, expected)) {
|
|
1345
|
+
return refuse("unexpected-address");
|
|
1346
|
+
}
|
|
1347
|
+
return { valid: true, call, source };
|
|
1348
|
+
}
|
|
1349
|
+
/**
|
|
1350
|
+
* The packed `transactions` bytes: per call, `uint8 operation (0) ‖ address to
|
|
1351
|
+
* ‖ uint256 value ‖ uint256 data length ‖ data`. Throws naming the first call
|
|
1352
|
+
* that is not a valid address, whole hex bytes and a uint256 value.
|
|
1353
|
+
*/
|
|
1354
|
+
export function packMultiSend(calls) {
|
|
1355
|
+
return concatHex(calls.map((call, index) => {
|
|
1356
|
+
const value = uint256(call?.value ?? 0n);
|
|
1357
|
+
if (typeof call?.to !== "string" ||
|
|
1358
|
+
!isAddress(call.to) ||
|
|
1359
|
+
!isHexBytes(call.data) ||
|
|
1360
|
+
value === null) {
|
|
1361
|
+
throw new Error(`Batch call ${index + 1} is not a valid call.`);
|
|
1362
|
+
}
|
|
1363
|
+
return encodePacked(["uint8", "address", "uint256", "uint256", "bytes"], [0, call.to, value, BigInt(size(call.data)), call.data]);
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
/** `multiSend(transactions)` calldata for at least one call. */
|
|
1367
|
+
export function encodeMultiSend(calls) {
|
|
1368
|
+
if (!calls.length)
|
|
1369
|
+
throw new Error("A batch needs at least one call.");
|
|
1370
|
+
return encodeFunctionData({
|
|
1371
|
+
abi: MULTI_SEND_ABI,
|
|
1372
|
+
functionName: "multiSend",
|
|
1373
|
+
args: [packMultiSend(calls)],
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* The calls in `multiSend` calldata, or null unless the bytes are exactly a
|
|
1378
|
+
* canonical `multiSend(bytes)` of one or more whole CALL entries. A
|
|
1379
|
+
* DELEGATECALL entry is null: MultiSendCallOnly reverts on it.
|
|
1380
|
+
*/
|
|
1381
|
+
export function decodeMultiSend(data) {
|
|
1382
|
+
if (!isHexBytes(data))
|
|
1383
|
+
return null;
|
|
1384
|
+
let packed;
|
|
1385
|
+
try {
|
|
1386
|
+
const decoded = decodeFunctionData({ abi: MULTI_SEND_ABI, data });
|
|
1387
|
+
packed = decoded.args[0];
|
|
1388
|
+
if (encodeFunctionData({
|
|
1389
|
+
abi: MULTI_SEND_ABI,
|
|
1390
|
+
functionName: "multiSend",
|
|
1391
|
+
args: [packed],
|
|
1392
|
+
}).toLowerCase() !== data.toLowerCase()) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
catch {
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
const bytes = packed.slice(2).toLowerCase();
|
|
1400
|
+
const calls = [];
|
|
1401
|
+
let offset = 0;
|
|
1402
|
+
while (offset < bytes.length) {
|
|
1403
|
+
// 1 + 20 + 32 + 32 header bytes before the call data.
|
|
1404
|
+
if (bytes.length - offset < 170 ||
|
|
1405
|
+
bytes.slice(offset, offset + 2) !== "00") {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
const length = BigInt(`0x${bytes.slice(offset + 106, offset + 170)}`);
|
|
1409
|
+
const end = BigInt(offset + 170) + length * 2n;
|
|
1410
|
+
if (end > BigInt(bytes.length))
|
|
1411
|
+
return null;
|
|
1412
|
+
calls.push({
|
|
1413
|
+
to: getAddress(`0x${bytes.slice(offset + 2, offset + 42)}`),
|
|
1414
|
+
value: BigInt(`0x${bytes.slice(offset + 42, offset + 106)}`),
|
|
1415
|
+
data: `0x${bytes.slice(offset + 170, Number(end))}`,
|
|
1416
|
+
});
|
|
1417
|
+
offset = Number(end);
|
|
1418
|
+
}
|
|
1419
|
+
return calls.length ? calls : null;
|
|
1420
|
+
}
|
|
1421
|
+
/** The calls of a Safe transaction that DELEGATECALLs MultiSendCallOnly, else null. */
|
|
1422
|
+
export function multiSendCallsOf(tx) {
|
|
1423
|
+
return Number(tx?.operation) === 1 &&
|
|
1424
|
+
typeof tx.to === "string" &&
|
|
1425
|
+
isAddress(tx.to) &&
|
|
1426
|
+
isAddressEqual(tx.to, MULTI_SEND_CALL_ONLY)
|
|
1427
|
+
? decodeMultiSend(tx.data)
|
|
1428
|
+
: null;
|
|
1429
|
+
}
|
|
389
1430
|
//# sourceMappingURL=safe.js.map
|