gate-evm-tools 1.0.21 → 1.0.23
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/README.md +47 -0
- package/dist/core/BytecodeChecker.d.ts +14 -46
- package/dist/core/BytecodeChecker.d.ts.map +1 -1
- package/dist/core/BytecodeChecker.js +131 -242
- package/dist/core/BytecodeChecker.js.map +1 -1
- package/dist/custody/credential.d.ts +16 -0
- package/dist/custody/credential.d.ts.map +1 -0
- package/dist/custody/credential.js +68 -0
- package/dist/custody/credential.js.map +1 -0
- package/dist/custody/hardhat-runtime.d.ts +16 -0
- package/dist/custody/hardhat-runtime.d.ts.map +1 -0
- package/dist/custody/hardhat-runtime.js +101 -0
- package/dist/custody/hardhat-runtime.js.map +1 -0
- package/dist/custody/hardhat.d.ts +3 -0
- package/dist/custody/hardhat.d.ts.map +1 -0
- package/dist/custody/hardhat.js +8 -0
- package/dist/custody/hardhat.js.map +1 -0
- package/dist/custody/index.d.ts +6 -0
- package/dist/custody/index.d.ts.map +1 -0
- package/dist/custody/index.js +6 -0
- package/dist/custody/index.js.map +1 -0
- package/dist/custody/service.d.ts +16 -0
- package/dist/custody/service.d.ts.map +1 -0
- package/dist/custody/service.js +100 -0
- package/dist/custody/service.js.map +1 -0
- package/dist/custody/signer.d.ts +13 -0
- package/dist/custody/signer.d.ts.map +1 -0
- package/dist/custody/signer.js +258 -0
- package/dist/custody/signer.js.map +1 -0
- package/dist/custody/version.d.ts +4 -0
- package/dist/custody/version.d.ts.map +1 -0
- package/dist/custody/version.js +13 -0
- package/dist/custody/version.js.map +1 -0
- package/dist-cjs/custody/credential.js +74 -0
- package/dist-cjs/custody/credential.js.map +1 -0
- package/dist-cjs/custody/hardhat-runtime.js +104 -0
- package/dist-cjs/custody/hardhat-runtime.js.map +1 -0
- package/dist-cjs/custody/hardhat.js +15 -0
- package/dist-cjs/custody/hardhat.js.map +1 -0
- package/dist-cjs/custody/index.js +18 -0
- package/dist-cjs/custody/index.js.map +1 -0
- package/dist-cjs/custody/service.js +105 -0
- package/dist-cjs/custody/service.js.map +1 -0
- package/dist-cjs/custody/signer.js +261 -0
- package/dist-cjs/custody/signer.js.map +1 -0
- package/dist-cjs/custody/version.js +16 -0
- package/dist-cjs/custody/version.js.map +1 -0
- package/dist-cjs/package.json +3 -0
- package/package.json +32 -4
package/README.md
CHANGED
|
@@ -163,6 +163,53 @@ module.exports = {
|
|
|
163
163
|
};
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
#### 托管钱包 Signer(ethers v5 / v6)
|
|
167
|
+
|
|
168
|
+
`gate-evm-tools` 支持把真实私钥和托管钱包统一放在同一个配置项中:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
# 真实私钥
|
|
172
|
+
PRIVATE_KEY=0x...
|
|
173
|
+
|
|
174
|
+
# 或托管钱包凭证
|
|
175
|
+
PRIVATE_KEY=custody:<ProjectSecret>:<WalletAddress>
|
|
176
|
+
CUSTODY_API_BASE_URL=https://custody.example.com
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`CUSTODY_API_BASE_URL` must be an HTTP(S) base URL without embedded
|
|
180
|
+
credentials, query parameters, or fragments.
|
|
181
|
+
|
|
182
|
+
也可以用 `WALLET_CREDENTIAL` 代替 `PRIVATE_KEY`。如果两个变量同时存在,值必须一致,否则会拒绝启动,避免误用账户。
|
|
183
|
+
|
|
184
|
+
Hardhat 配置需要在 ethers 和 OpenZeppelin 插件之后加载托管插件,并用 `resolveHardhatAccounts` 处理网络账户:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import "@nomicfoundation/hardhat-ethers";
|
|
188
|
+
import "@openzeppelin/hardhat-upgrades";
|
|
189
|
+
import "gate-evm-tools/hardhat";
|
|
190
|
+
import { resolveHardhatAccounts } from "gate-evm-tools/hardhat";
|
|
191
|
+
|
|
192
|
+
const walletCredential =
|
|
193
|
+
process.env.WALLET_CREDENTIAL ?? process.env.PRIVATE_KEY ?? "";
|
|
194
|
+
|
|
195
|
+
export default {
|
|
196
|
+
networks: {
|
|
197
|
+
sepolia: {
|
|
198
|
+
url: process.env.SEPOLIA_RPC_URL,
|
|
199
|
+
accounts: resolveHardhatAccounts(walletCredential),
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- 真实私钥返回给原生 `networks.accounts`,部署行为保持不变。
|
|
206
|
+
- 托管凭证让原生 accounts 保持为空,并自动替换 `ethers.getSigners()`、匹配地址的 `ethers.getSigner()` 和 `ethers.provider.getSigner(0)`。
|
|
207
|
+
- 实际 ethers 主版本取自 `hre.ethers.version`;v5 使用运行时 `Signer`,v6 使用运行时 `AbstractSigner`,包内不会固定加载某个 ethers 版本。
|
|
208
|
+
- 业务部署脚本无需传入额外 signer;`ethers.getContractFactory()`、`deployContract()` 和 `upgrades.deployProxy()` 会取得托管 signer。
|
|
209
|
+
- 完整托管凭证通过 Bearer 发送,服务端拆解 Project Secret 与绑定钱包地址;凭证不会写入日志,每次签名请求都会生成独立的 `Idempotency-Key`。
|
|
210
|
+
|
|
211
|
+
托管服务只返回签名或 raw transaction,不负责广播。交易最终仍由当前 Hardhat ethers Provider 广播。现有 OpenZeppelin CREATE2/Multicall 补丁与 signer 注入是两套独立能力,升级项目应继续保留原补丁。
|
|
212
|
+
|
|
166
213
|
### 4. 编译合约
|
|
167
214
|
|
|
168
215
|
```bash
|
|
@@ -3,16 +3,13 @@ type HardhatRuntimeEnvironment = any;
|
|
|
3
3
|
interface LocalBytecodeData {
|
|
4
4
|
bytecode: string;
|
|
5
5
|
abi: any[];
|
|
6
|
+
immutableReferences: ImmutableReferences;
|
|
6
7
|
}
|
|
7
|
-
interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
inputs?: ConstructorInput[];
|
|
11
|
-
}
|
|
12
|
-
interface Constructor {
|
|
13
|
-
type: string;
|
|
14
|
-
inputs: ConstructorInput[];
|
|
8
|
+
interface ImmutableReference {
|
|
9
|
+
start: number;
|
|
10
|
+
length: number;
|
|
15
11
|
}
|
|
12
|
+
type ImmutableReferences = Record<string, ImmutableReference[]>;
|
|
16
13
|
interface LibraryLinkInfo {
|
|
17
14
|
placeholder: string;
|
|
18
15
|
position: number;
|
|
@@ -21,15 +18,8 @@ interface LibraryLinkInfo {
|
|
|
21
18
|
interface ComparisonResult {
|
|
22
19
|
matches: boolean;
|
|
23
20
|
reason: string;
|
|
24
|
-
constructorDetails?: any[];
|
|
25
21
|
libraryLinks?: LibraryLinkInfo[];
|
|
26
22
|
}
|
|
27
|
-
interface ConstructorCheckResult {
|
|
28
|
-
isConstructorDiff: boolean;
|
|
29
|
-
reason: string;
|
|
30
|
-
differences?: any[];
|
|
31
|
-
diffCount?: number;
|
|
32
|
-
}
|
|
33
23
|
export declare class BytecodeChecker {
|
|
34
24
|
private hre;
|
|
35
25
|
private addressData;
|
|
@@ -61,37 +51,11 @@ export declare class BytecodeChecker {
|
|
|
61
51
|
*/
|
|
62
52
|
private getProxyLocalBytecode;
|
|
63
53
|
/**
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 基于ABI的智能分析
|
|
54
|
+
* 使用 artifact 的 fully-qualified identity 读取编译器生成的 immutableReferences。
|
|
55
|
+
* build-info 缺失时保留空引用,使完全一致的 bytecode 仍可通过;一旦存在差异,
|
|
56
|
+
* compareBytecode 会按 fail-closed 规则拒绝缺少编译器证据的匹配。
|
|
69
57
|
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 检测immutable变量
|
|
73
|
-
*/
|
|
74
|
-
detectImmutableVariables(onchainCode: string, localCode: string): ConstructorCheckResult;
|
|
75
|
-
/**
|
|
76
|
-
* 检测constructor参数
|
|
77
|
-
*/
|
|
78
|
-
detectConstructorParams(onchainCode: string, localCode: string, constructor: Constructor): ConstructorCheckResult;
|
|
79
|
-
/**
|
|
80
|
-
* 去重参数:按顺序匹配参数
|
|
81
|
-
*/
|
|
82
|
-
deduplicateParams(allMatches: any[], params: ConstructorInput[]): any[];
|
|
83
|
-
/**
|
|
84
|
-
* 查找地址参数
|
|
85
|
-
*/
|
|
86
|
-
findAddressParams(onchainCode: string, localCode: string, param: ConstructorInput): any[];
|
|
87
|
-
/**
|
|
88
|
-
* 查找数值参数
|
|
89
|
-
*/
|
|
90
|
-
findUintParams(onchainCode: string, localCode: string, param: ConstructorInput): any[];
|
|
91
|
-
/**
|
|
92
|
-
* 回退分析方法
|
|
93
|
-
*/
|
|
94
|
-
fallbackAnalyze(onchainCode: string, localCode: string): ConstructorCheckResult;
|
|
58
|
+
private getArtifactLocalBytecode;
|
|
95
59
|
/**
|
|
96
60
|
* 标准化bytecode(移除metadata)
|
|
97
61
|
*/
|
|
@@ -129,10 +93,14 @@ export declare class BytecodeChecker {
|
|
|
129
93
|
local: string;
|
|
130
94
|
selfAddress: string;
|
|
131
95
|
} | null;
|
|
96
|
+
/**
|
|
97
|
+
* 只屏蔽编译器声明的 immutable 区间,并比较其余 runtime bytecode。
|
|
98
|
+
*/
|
|
99
|
+
private compareWithImmutableReferences;
|
|
132
100
|
/**
|
|
133
101
|
* 比较两个bytecode
|
|
134
102
|
*/
|
|
135
|
-
compareBytecode(onchainBytecode: string, localBytecode: string,
|
|
103
|
+
compareBytecode(onchainBytecode: string, localBytecode: string, _abi?: any[] | null, immutableReferences?: ImmutableReferences): ComparisonResult;
|
|
136
104
|
/**
|
|
137
105
|
* 检查单个合约
|
|
138
106
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BytecodeChecker.d.ts","sourceRoot":"","sources":["../../src/core/BytecodeChecker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D,KAAK,yBAAyB,GAAG,GAAG,CAAC;AAErC,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"BytecodeChecker.d.ts","sourceRoot":"","sources":["../../src/core/BytecodeChecker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D,KAAK,yBAAyB,GAAG,GAAG,CAAC;AAErC,UAAU,iBAAiB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,GAAG,EAAE,CAAC;IACX,mBAAmB,EAAE,mBAAmB,CAAC;CAC1C;AAED,UAAU,kBAAkB;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,kBAAkB,EAAE,CAAC,CAAC;AAEhE,UAAU,eAAe;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,gBAAgB;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;CAClC;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,WAAW,CAAiB;gBAExB,GAAG,EAAE,yBAAyB,EAAE,WAAW,EAAE,cAAc;IAKvE;;;;;;;;;OASG;IACH,OAAO,CAAC,iBAAiB;IAuBzB;;OAEG;IACG,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgB3E;;;;OAIG;IACG,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAaxE;;;OAGG;YACW,qBAAqB;IAWnC;;;;OAIG;YACW,wBAAwB;IAsCtC;;OAEG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAiB3C;;;;;;;;OAQG;IACH,4BAA4B,CAC1B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,eAAe,EAAE,CAAA;KAAE;IAqCtE;;;;;;;;;;;;;OAaG;IACH,2BAA2B,CACzB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAmBjE;;OAEG;IACH,OAAO,CAAC,8BAA8B;IAoHtC;;OAEG;IACH,eAAe,CACb,eAAe,EAAE,MAAM,EACvB,aAAa,EAAE,MAAM,EACrB,IAAI,GAAE,GAAG,EAAE,GAAG,IAAW,EACzB,mBAAmB,GAAE,mBAAwB,GAC5C,gBAAgB;IA6DnB;;;;;OAKG;IACG,aAAa,CAAC,YAAY,EAAE;QAChC,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,WAAW,CAAC;IAuDxB;;OAEG;IACG,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAsBxE;;OAEG;IACG,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAuBzE;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAejD;;OAEG;IACH,8BAA8B,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CA4B5F"}
|
|
@@ -62,11 +62,7 @@ export class BytecodeChecker {
|
|
|
62
62
|
if (contractName.endsWith('::Proxy')) {
|
|
63
63
|
return await this.getProxyLocalBytecode();
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
bytecode: artifact.deployedBytecode,
|
|
68
|
-
abi: artifact.abi
|
|
69
|
-
};
|
|
65
|
+
return await this.getArtifactLocalBytecode(contractName);
|
|
70
66
|
}
|
|
71
67
|
catch (error) {
|
|
72
68
|
throw new Error(`Failed to get local bytecode: ${error.message}`);
|
|
@@ -78,11 +74,7 @@ export class BytecodeChecker {
|
|
|
78
74
|
*/
|
|
79
75
|
async getProxyLocalBytecode() {
|
|
80
76
|
try {
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
bytecode: artifact.deployedBytecode,
|
|
84
|
-
abi: artifact.abi
|
|
85
|
-
};
|
|
77
|
+
return await this.getArtifactLocalBytecode('ERC1967Proxy');
|
|
86
78
|
}
|
|
87
79
|
catch (error) {
|
|
88
80
|
throw new Error(`无法找到 ERC1967Proxy 的本地 artifact。\n` +
|
|
@@ -90,225 +82,38 @@ export class BytecodeChecker {
|
|
|
90
82
|
}
|
|
91
83
|
}
|
|
92
84
|
/**
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (onchainCode.length !== localCode.length) {
|
|
97
|
-
return { isConstructorDiff: false, reason: "长度不同" };
|
|
98
|
-
}
|
|
99
|
-
if (onchainCode === localCode) {
|
|
100
|
-
return { isConstructorDiff: false, reason: "完全相同" };
|
|
101
|
-
}
|
|
102
|
-
if (abi) {
|
|
103
|
-
const smartAnalysis = this.smartAnalyzeWithABI(onchainCode, localCode, abi);
|
|
104
|
-
if (smartAnalysis.isConstructorDiff) {
|
|
105
|
-
return smartAnalysis;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return this.fallbackAnalyze(onchainCode, localCode);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* 基于ABI的智能分析
|
|
112
|
-
*/
|
|
113
|
-
smartAnalyzeWithABI(onchainCode, localCode, abi) {
|
|
114
|
-
const constructor = abi.find(item => item.type === 'constructor');
|
|
115
|
-
if (!constructor || !constructor.inputs || constructor.inputs.length === 0) {
|
|
116
|
-
return this.detectImmutableVariables(onchainCode, localCode);
|
|
117
|
-
}
|
|
118
|
-
return this.detectConstructorParams(onchainCode, localCode, constructor);
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* 检测immutable变量
|
|
85
|
+
* 使用 artifact 的 fully-qualified identity 读取编译器生成的 immutableReferences。
|
|
86
|
+
* build-info 缺失时保留空引用,使完全一致的 bytecode 仍可通过;一旦存在差异,
|
|
87
|
+
* compareBytecode 会按 fail-closed 规则拒绝缺少编译器证据的匹配。
|
|
122
88
|
*/
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
description: `Immutable地址变量: 0x${address}`
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
try {
|
|
146
|
-
const value = BigInt('0x' + onchainSegment);
|
|
147
|
-
matches.push({
|
|
148
|
-
position: position,
|
|
149
|
-
length: 64,
|
|
150
|
-
onchainValue: onchainSegment,
|
|
151
|
-
localValue: localSegment,
|
|
152
|
-
type: "uint256",
|
|
153
|
-
description: `Immutable uint256变量: ${value.toString()}`
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
catch (e) {
|
|
157
|
-
// 忽略无法解析的值
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
if (matches.length > 0) {
|
|
163
|
-
const descriptions = matches.map(m => m.description).join(", ");
|
|
164
|
-
return {
|
|
165
|
-
isConstructorDiff: true,
|
|
166
|
-
reason: `Immutable变量差异: ${descriptions}`,
|
|
167
|
-
differences: matches,
|
|
168
|
-
diffCount: matches.reduce((sum, m) => sum + m.length, 0)
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return { isConstructorDiff: false, reason: "未检测到immutable变量模式" };
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* 检测constructor参数
|
|
175
|
-
*/
|
|
176
|
-
detectConstructorParams(onchainCode, localCode, constructor) {
|
|
177
|
-
const params = constructor.inputs;
|
|
178
|
-
const allMatches = [];
|
|
179
|
-
for (const param of params) {
|
|
180
|
-
if (param.type === 'address') {
|
|
181
|
-
const addressMatches = this.findAddressParams(onchainCode, localCode, param);
|
|
182
|
-
allMatches.push(...addressMatches);
|
|
183
|
-
}
|
|
184
|
-
else if (param.type.startsWith('uint')) {
|
|
185
|
-
const uintMatches = this.findUintParams(onchainCode, localCode, param);
|
|
186
|
-
allMatches.push(...uintMatches);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (allMatches.length > 0) {
|
|
190
|
-
const uniqueParams = this.deduplicateParams(allMatches, params);
|
|
191
|
-
const descriptions = uniqueParams.map(p => p.description).join(", ");
|
|
192
|
-
return {
|
|
193
|
-
isConstructorDiff: true,
|
|
194
|
-
reason: `Constructor参数差异(${params.map(p => `${p.type} ${p.name}`).join(', ')}): ${descriptions}`,
|
|
195
|
-
differences: uniqueParams,
|
|
196
|
-
diffCount: allMatches.reduce((sum, m) => sum + m.length, 0)
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
return { isConstructorDiff: false, reason: "未检测到constructor参数模式" };
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* 去重参数:按顺序匹配参数
|
|
203
|
-
*/
|
|
204
|
-
deduplicateParams(allMatches, params) {
|
|
205
|
-
const uniqueAddresses = [];
|
|
206
|
-
const seenAddresses = new Set();
|
|
207
|
-
for (const match of allMatches) {
|
|
208
|
-
const address = match.address || match.onchainValue;
|
|
209
|
-
if (!seenAddresses.has(address)) {
|
|
210
|
-
seenAddresses.add(address);
|
|
211
|
-
uniqueAddresses.push({
|
|
212
|
-
address: address,
|
|
213
|
-
match: match
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
const uniqueParams = [];
|
|
218
|
-
const addressParams = params.filter(p => p.type === 'address');
|
|
219
|
-
const reversedAddresses = [...uniqueAddresses].reverse();
|
|
220
|
-
for (let i = 0; i < addressParams.length && i < reversedAddresses.length; i++) {
|
|
221
|
-
const param = addressParams[i];
|
|
222
|
-
const addressInfo = reversedAddresses[i];
|
|
223
|
-
uniqueParams.push({
|
|
224
|
-
...addressInfo.match,
|
|
225
|
-
description: `${param.name}(${param.type}): 0x${addressInfo.address}`
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
return uniqueParams;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* 查找地址参数
|
|
232
|
-
*/
|
|
233
|
-
findAddressParams(onchainCode, localCode, param) {
|
|
234
|
-
const matches = [];
|
|
235
|
-
const zeroPattern64 = /0{64}/g;
|
|
236
|
-
let match;
|
|
237
|
-
while ((match = zeroPattern64.exec(localCode)) !== null) {
|
|
238
|
-
const position = match.index;
|
|
239
|
-
const onchainSegment = onchainCode.substr(position, 64);
|
|
240
|
-
const localSegment = match[0];
|
|
241
|
-
if (onchainSegment !== localSegment && /^[0-9a-f]{64}$/i.test(onchainSegment)) {
|
|
242
|
-
if (onchainSegment.startsWith('000000000000000000000000')) {
|
|
243
|
-
const address = onchainSegment.substr(24, 40);
|
|
244
|
-
if (address !== '0000000000000000000000000000000000000000') {
|
|
245
|
-
const alreadyExists = matches.some(m => m.address === address && m.position === position);
|
|
246
|
-
if (!alreadyExists) {
|
|
247
|
-
matches.push({
|
|
248
|
-
position: position,
|
|
249
|
-
length: 64,
|
|
250
|
-
onchainValue: onchainSegment,
|
|
251
|
-
localValue: localSegment,
|
|
252
|
-
type: "address",
|
|
253
|
-
address: address,
|
|
254
|
-
description: `${param.name}(${param.type}): 0x${address}`
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
89
|
+
async getArtifactLocalBytecode(contractName) {
|
|
90
|
+
const artifact = await this.hre.artifacts.readArtifact(contractName);
|
|
91
|
+
let immutableReferences = {};
|
|
92
|
+
const sourceName = artifact.sourceName;
|
|
93
|
+
const artifactContractName = artifact.contractName;
|
|
94
|
+
const getBuildInfo = this.hre.artifacts.getBuildInfo;
|
|
95
|
+
if (typeof sourceName === 'string' &&
|
|
96
|
+
typeof artifactContractName === 'string' &&
|
|
97
|
+
typeof getBuildInfo === 'function') {
|
|
98
|
+
try {
|
|
99
|
+
const fullyQualifiedName = `${sourceName}:${artifactContractName}`;
|
|
100
|
+
const buildInfo = await getBuildInfo.call(this.hre.artifacts, fullyQualifiedName);
|
|
101
|
+
const compilerReferences = buildInfo?.output?.contracts?.[sourceName]?.[artifactContractName]
|
|
102
|
+
?.evm?.deployedBytecode?.immutableReferences;
|
|
103
|
+
if (compilerReferences &&
|
|
104
|
+
typeof compilerReferences === 'object' &&
|
|
105
|
+
!Array.isArray(compilerReferences)) {
|
|
106
|
+
immutableReferences = compilerReferences;
|
|
258
107
|
}
|
|
259
108
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* 查找数值参数
|
|
265
|
-
*/
|
|
266
|
-
findUintParams(onchainCode, localCode, param) {
|
|
267
|
-
const matches = [];
|
|
268
|
-
const bitSize = param.type === 'uint' ? 256 : parseInt(param.type.replace('uint', ''));
|
|
269
|
-
const hexLength = bitSize / 4;
|
|
270
|
-
const zeroPattern = new RegExp(`0{${hexLength}}`, 'g');
|
|
271
|
-
let match;
|
|
272
|
-
while ((match = zeroPattern.exec(localCode)) !== null) {
|
|
273
|
-
const position = match.index;
|
|
274
|
-
const onchainSegment = onchainCode.substr(position, hexLength);
|
|
275
|
-
if (onchainSegment !== match[0] && new RegExp(`^[0-9a-f]{${hexLength}}$`, 'i').test(onchainSegment)) {
|
|
276
|
-
try {
|
|
277
|
-
const value = BigInt('0x' + onchainSegment);
|
|
278
|
-
matches.push({
|
|
279
|
-
position: position,
|
|
280
|
-
length: hexLength,
|
|
281
|
-
onchainValue: onchainSegment,
|
|
282
|
-
localValue: match[0],
|
|
283
|
-
type: param.type,
|
|
284
|
-
description: `${param.name}(${param.type}): ${value.toString()}`
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
catch (e) {
|
|
288
|
-
// 忽略无法解析的值
|
|
289
|
-
}
|
|
109
|
+
catch {
|
|
110
|
+
immutableReferences = {};
|
|
290
111
|
}
|
|
291
112
|
}
|
|
292
|
-
return matches;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* 回退分析方法
|
|
296
|
-
*/
|
|
297
|
-
fallbackAnalyze(onchainCode, localCode) {
|
|
298
|
-
const wethAddress = 'c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
|
|
299
|
-
const zeroAddress = '0000000000000000000000000000000000000000';
|
|
300
|
-
const onchainNormalized = onchainCode.replace(new RegExp(wethAddress, 'g'), zeroAddress);
|
|
301
|
-
if (onchainNormalized === localCode) {
|
|
302
|
-
return {
|
|
303
|
-
isConstructorDiff: true,
|
|
304
|
-
reason: "Constructor参数差异: WETH地址",
|
|
305
|
-
differences: [{ description: `WETH地址: 0x${wethAddress}` }],
|
|
306
|
-
diffCount: 40
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
113
|
return {
|
|
310
|
-
|
|
311
|
-
|
|
114
|
+
bytecode: artifact.deployedBytecode,
|
|
115
|
+
abi: artifact.abi,
|
|
116
|
+
immutableReferences
|
|
312
117
|
};
|
|
313
118
|
}
|
|
314
119
|
/**
|
|
@@ -395,10 +200,101 @@ export class BytecodeChecker {
|
|
|
395
200
|
const normalizedOnchain = '73' + zeroFill + onchainCode.substring(42);
|
|
396
201
|
return { onchain: normalizedOnchain, local: localCode, selfAddress: `0x${selfAddress}` };
|
|
397
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* 只屏蔽编译器声明的 immutable 区间,并比较其余 runtime bytecode。
|
|
205
|
+
*/
|
|
206
|
+
compareWithImmutableReferences(onchainCode, localCode, immutableReferences) {
|
|
207
|
+
if (!immutableReferences ||
|
|
208
|
+
typeof immutableReferences !== 'object' ||
|
|
209
|
+
Array.isArray(immutableReferences) ||
|
|
210
|
+
Object.keys(immutableReferences).length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
matches: false,
|
|
213
|
+
reason: '字节码不匹配,compiler immutableReferences 缺失'
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const ranges = [];
|
|
217
|
+
for (const [id, references] of Object.entries(immutableReferences)) {
|
|
218
|
+
if (!Array.isArray(references) || references.length === 0) {
|
|
219
|
+
return {
|
|
220
|
+
matches: false,
|
|
221
|
+
reason: `compiler immutable ${id} 引用列表无效`
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const onchainValuesByLength = new Map();
|
|
225
|
+
for (let index = 0; index < references.length; index++) {
|
|
226
|
+
const reference = references[index];
|
|
227
|
+
if (!reference ||
|
|
228
|
+
!Number.isInteger(reference.start) ||
|
|
229
|
+
!Number.isInteger(reference.length) ||
|
|
230
|
+
reference.start < 0 ||
|
|
231
|
+
reference.length <= 0) {
|
|
232
|
+
return {
|
|
233
|
+
matches: false,
|
|
234
|
+
reason: `compiler immutable ${id}[${index}] 元数据无效:start 必须是非负整数,length 必须是正整数`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const startHex = reference.start * 2;
|
|
238
|
+
const lengthHex = reference.length * 2;
|
|
239
|
+
const endHex = startHex + lengthHex;
|
|
240
|
+
if (endHex > onchainCode.length || endHex > localCode.length) {
|
|
241
|
+
return {
|
|
242
|
+
matches: false,
|
|
243
|
+
reason: `compiler immutable ${id}[${index}] 引用越界`
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const onchainValue = onchainCode.slice(startHex, endHex);
|
|
247
|
+
const priorValue = onchainValuesByLength.get(reference.length);
|
|
248
|
+
if (priorValue !== undefined && priorValue !== onchainValue) {
|
|
249
|
+
return {
|
|
250
|
+
matches: false,
|
|
251
|
+
reason: `compiler immutable ${id} 的重复引用值不一致`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
onchainValuesByLength.set(reference.length, onchainValue);
|
|
255
|
+
ranges.push({ id, index, startHex, endHex, lengthHex });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (ranges.length === 0) {
|
|
259
|
+
return {
|
|
260
|
+
matches: false,
|
|
261
|
+
reason: '字节码不匹配,compiler immutableReferences 缺失'
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
ranges.sort((left, right) => left.startHex - right.startHex || left.endHex - right.endHex);
|
|
265
|
+
for (let index = 1; index < ranges.length; index++) {
|
|
266
|
+
const previous = ranges[index - 1];
|
|
267
|
+
const current = ranges[index];
|
|
268
|
+
if (current.startHex < previous.endHex) {
|
|
269
|
+
return {
|
|
270
|
+
matches: false,
|
|
271
|
+
reason: `compiler immutable 引用区间重叠:${previous.id}[${previous.index}] 与 ${current.id}[${current.index}]`
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
let maskedOnchain = onchainCode;
|
|
276
|
+
let maskedLocal = localCode;
|
|
277
|
+
for (let index = ranges.length - 1; index >= 0; index--) {
|
|
278
|
+
const range = ranges[index];
|
|
279
|
+
const mask = '0'.repeat(range.lengthHex);
|
|
280
|
+
maskedOnchain = maskedOnchain.slice(0, range.startHex) + mask + maskedOnchain.slice(range.endHex);
|
|
281
|
+
maskedLocal = maskedLocal.slice(0, range.startHex) + mask + maskedLocal.slice(range.endHex);
|
|
282
|
+
}
|
|
283
|
+
if (maskedOnchain !== maskedLocal) {
|
|
284
|
+
return {
|
|
285
|
+
matches: false,
|
|
286
|
+
reason: 'compiler immutableReferences 区间外仍存在代码差异'
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
matches: true,
|
|
291
|
+
reason: `编译器 immutableReferences 精确匹配(${ranges.length} 处)`
|
|
292
|
+
};
|
|
293
|
+
}
|
|
398
294
|
/**
|
|
399
295
|
* 比较两个bytecode
|
|
400
296
|
*/
|
|
401
|
-
compareBytecode(onchainBytecode, localBytecode,
|
|
297
|
+
compareBytecode(onchainBytecode, localBytecode, _abi = null, immutableReferences = {}) {
|
|
402
298
|
const onchainNormalized = this.normalizeBytecode(onchainBytecode);
|
|
403
299
|
let localNormalized = this.normalizeBytecode(localBytecode);
|
|
404
300
|
let libraryLinks = [];
|
|
@@ -436,23 +332,16 @@ export class BytecodeChecker {
|
|
|
436
332
|
libraryLinks: libraryLinks.length > 0 ? libraryLinks : undefined
|
|
437
333
|
};
|
|
438
334
|
}
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
matches: true,
|
|
451
|
-
reason: `前缀匹配(可能包含constructor参数)${extraSuffix}`,
|
|
452
|
-
libraryLinks: libraryLinks.length > 0 ? libraryLinks : undefined
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
return { matches: false, reason: "代码存在显著差异" };
|
|
335
|
+
const immutableComparison = this.compareWithImmutableReferences(onchainForCompare, localForCompare, immutableReferences);
|
|
336
|
+
return {
|
|
337
|
+
...immutableComparison,
|
|
338
|
+
reason: immutableComparison.matches
|
|
339
|
+
? `${immutableComparison.reason}${extraSuffix}`
|
|
340
|
+
: immutableComparison.reason,
|
|
341
|
+
libraryLinks: immutableComparison.matches && libraryLinks.length > 0
|
|
342
|
+
? libraryLinks
|
|
343
|
+
: undefined
|
|
344
|
+
};
|
|
456
345
|
}
|
|
457
346
|
/**
|
|
458
347
|
* 检查单个合约
|
|
@@ -490,7 +379,7 @@ export class BytecodeChecker {
|
|
|
490
379
|
// 使用实际合约名获取本地 bytecode
|
|
491
380
|
const localData = await this.getLocalBytecode(contractName);
|
|
492
381
|
result.localBytecodeLength = localData.bytecode.length;
|
|
493
|
-
const comparison = this.compareBytecode(onchainBytecode, localData.bytecode, localData.abi);
|
|
382
|
+
const comparison = this.compareBytecode(onchainBytecode, localData.bytecode, localData.abi, localData.immutableReferences);
|
|
494
383
|
result.matches = comparison.matches;
|
|
495
384
|
result.reason = comparison.reason;
|
|
496
385
|
}
|