@morpho-org/evm-simulation 4.1.4 → 4.1.6-next.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/lib/cjs/errors.d.ts +1 -1
- package/lib/cjs/errors.js +1 -1
- package/lib/cjs/simulate/asset-changes.d.ts +16 -1
- package/lib/cjs/simulate/asset-changes.js +20 -0
- package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +2 -1
- package/lib/cjs/simulate/backends/eth-simulate-v1.js +2 -2
- package/lib/cjs/simulate/backends/tenderly-rpc.js +1 -1
- package/lib/cjs/simulate/parsing/transfers.d.ts +29 -15
- package/lib/cjs/simulate/parsing/transfers.js +50 -55
- package/lib/cjs/simulate/pipeline/bundler-retention.d.ts +26 -8
- package/lib/cjs/simulate/pipeline/bundler-retention.js +66 -43
- package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +2 -1
- package/lib/cjs/simulate/pipeline/execute-simulation.js +3 -1
- package/lib/cjs/simulate/simulate.d.ts +8 -4
- package/lib/cjs/simulate/simulate.js +15 -5
- package/lib/esm/errors.d.ts +1 -1
- package/lib/esm/errors.js +1 -1
- package/lib/esm/simulate/asset-changes.d.ts +16 -1
- package/lib/esm/simulate/asset-changes.js +20 -1
- package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +2 -1
- package/lib/esm/simulate/backends/eth-simulate-v1.js +2 -2
- package/lib/esm/simulate/backends/tenderly-rpc.js +3 -3
- package/lib/esm/simulate/parsing/transfers.d.ts +29 -15
- package/lib/esm/simulate/parsing/transfers.js +51 -56
- package/lib/esm/simulate/pipeline/bundler-retention.d.ts +26 -8
- package/lib/esm/simulate/pipeline/bundler-retention.js +66 -43
- package/lib/esm/simulate/pipeline/execute-simulation.d.ts +2 -1
- package/lib/esm/simulate/pipeline/execute-simulation.js +3 -1
- package/lib/esm/simulate/simulate.d.ts +8 -4
- package/lib/esm/simulate/simulate.js +15 -5
- package/package.json +5 -5
package/lib/cjs/errors.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ interface RetainedAsset {
|
|
|
19
19
|
token: string | undefined;
|
|
20
20
|
netRetained: string;
|
|
21
21
|
}
|
|
22
|
-
/** Funds would flow to bundler3 contract addresses. Never bypassable. */
|
|
22
|
+
/** Funds would flow to restricted bundler3 or bundles contract addresses. Never bypassable. */
|
|
23
23
|
export declare class BlacklistViolationError extends SimulationPackageError {
|
|
24
24
|
readonly assetChanges?: RetainedAsset[] | undefined;
|
|
25
25
|
readonly code = "BLACKLIST_ERROR";
|
package/lib/cjs/errors.js
CHANGED
|
@@ -24,7 +24,7 @@ class SimulationRevertedError extends SimulationPackageError {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
exports.SimulationRevertedError = SimulationRevertedError;
|
|
27
|
-
/** Funds would flow to bundler3 contract addresses. Never bypassable. */
|
|
27
|
+
/** Funds would flow to restricted bundler3 or bundles contract addresses. Never bypassable. */
|
|
28
28
|
class BlacklistViolationError extends SimulationPackageError {
|
|
29
29
|
assetChanges;
|
|
30
30
|
code = "BLACKLIST_ERROR";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Address } from "viem";
|
|
1
|
+
import { type Address, type Hex } from "viem";
|
|
2
2
|
import type { AccountAssetChanges } from "../types.js";
|
|
3
3
|
/** A single signed contribution to one account's balance for one token. */
|
|
4
4
|
export interface AssetChangeEntry {
|
|
@@ -8,6 +8,21 @@ export interface AssetChangeEntry {
|
|
|
8
8
|
symbol?: string;
|
|
9
9
|
decimals?: number;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Collapse the native-ETH sentinel to viem's lowercase `ethAddress`, checksumming
|
|
13
|
+
* any real token address. `eth_simulateV1` synthesizes native moves from the
|
|
14
|
+
* sentinel `0xeee…eee`, and Tenderly may echo it — in checksummed or any other
|
|
15
|
+
* case — as an asset change's `contractAddress`. Native ETH is keyed by the exact
|
|
16
|
+
* `ethAddress` constant here and in the bundler-retention guard, so a checksummed
|
|
17
|
+
* sentinel would land on a separate map key and silently escape retention checks.
|
|
18
|
+
* This is the single source of truth for token normalization shared by the
|
|
19
|
+
* transfer-log parser and the Tenderly asset-change mapper.
|
|
20
|
+
*
|
|
21
|
+
* @param address - Token address emitting a transfer or carried by an asset change.
|
|
22
|
+
* @returns `ethAddress` for the native sentinel, else the checksummed address.
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeAssetToken(address: Hex): Address;
|
|
11
26
|
/**
|
|
12
27
|
* Net signed per-(account, token) contributions into balance changes grouped by
|
|
13
28
|
* account. Accounts and their `changes` are sorted by address for deterministic
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeAssetToken = normalizeAssetToken;
|
|
3
4
|
exports.groupAssetChanges = groupAssetChanges;
|
|
4
5
|
const viem_1 = require("viem");
|
|
6
|
+
/**
|
|
7
|
+
* Collapse the native-ETH sentinel to viem's lowercase `ethAddress`, checksumming
|
|
8
|
+
* any real token address. `eth_simulateV1` synthesizes native moves from the
|
|
9
|
+
* sentinel `0xeee…eee`, and Tenderly may echo it — in checksummed or any other
|
|
10
|
+
* case — as an asset change's `contractAddress`. Native ETH is keyed by the exact
|
|
11
|
+
* `ethAddress` constant here and in the bundler-retention guard, so a checksummed
|
|
12
|
+
* sentinel would land on a separate map key and silently escape retention checks.
|
|
13
|
+
* This is the single source of truth for token normalization shared by the
|
|
14
|
+
* transfer-log parser and the Tenderly asset-change mapper.
|
|
15
|
+
*
|
|
16
|
+
* @param address - Token address emitting a transfer or carried by an asset change.
|
|
17
|
+
* @returns `ethAddress` for the native sentinel, else the checksummed address.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
function normalizeAssetToken(address) {
|
|
21
|
+
return address.toLowerCase() === viem_1.ethAddress
|
|
22
|
+
? viem_1.ethAddress
|
|
23
|
+
: (0, viem_1.getAddress)(address);
|
|
24
|
+
}
|
|
5
25
|
// Locale-independent byte-order compare on lowercased hex, matching `sortTransfers`.
|
|
6
26
|
const compareAddress = (a, b) => {
|
|
7
27
|
const al = a.toLowerCase();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BlockTag } from "viem";
|
|
1
|
+
import { type Address, type BlockTag } from "viem";
|
|
2
2
|
import type { RawSimulationResult, SimulationTransaction } from "../../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Simulate transactions using eth_simulateV1 (viem simulateCalls).
|
|
@@ -19,5 +19,6 @@ export declare function simulateV1(params: {
|
|
|
19
19
|
chainId: number;
|
|
20
20
|
transactions: SimulationTransaction[];
|
|
21
21
|
blockNumber?: bigint | BlockTag;
|
|
22
|
+
wNative?: Address | null;
|
|
22
23
|
signal?: AbortSignal;
|
|
23
24
|
}): Promise<RawSimulationResult>;
|
|
@@ -20,7 +20,7 @@ const index_js_1 = require("../parsing/index.js");
|
|
|
20
20
|
* `traceTransfers` already logs it, so adding it would double-count.
|
|
21
21
|
*/
|
|
22
22
|
async function simulateV1(params) {
|
|
23
|
-
const { rpcUrl, transactions, blockNumber, signal } = params;
|
|
23
|
+
const { rpcUrl, transactions, blockNumber, wNative, signal } = params;
|
|
24
24
|
const client = (0, viem_1.createPublicClient)({
|
|
25
25
|
transport: (0, viem_1.http)(rpcUrl, {
|
|
26
26
|
fetchOptions: signal ? { signal } : undefined,
|
|
@@ -84,7 +84,7 @@ async function simulateV1(params) {
|
|
|
84
84
|
}));
|
|
85
85
|
return {
|
|
86
86
|
calls: rawCalls,
|
|
87
|
-
assetChanges: toAssetChanges((0, index_js_1.parseTransfers)(rawCalls)),
|
|
87
|
+
assetChanges: toAssetChanges((0, index_js_1.parseTransfers)(rawCalls, { wNative })),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
catch (error) {
|
|
@@ -209,7 +209,7 @@ function toAssetChanges(results) {
|
|
|
209
209
|
for (const change of result.assetChanges ?? []) {
|
|
210
210
|
const amount = BigInt(change.rawAmount);
|
|
211
211
|
const token = change.assetInfo.contractAddress
|
|
212
|
-
? (0,
|
|
212
|
+
? (0, asset_changes_js_1.normalizeAssetToken)(change.assetInfo.contractAddress)
|
|
213
213
|
: viem_1.ethAddress;
|
|
214
214
|
const { symbol, decimals } = change.assetInfo;
|
|
215
215
|
if (change.to)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Address } from "viem";
|
|
1
2
|
import type { RawCall, SimulationLogger, Transfer } from "../../types.js";
|
|
2
3
|
export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
3
4
|
export declare const WITHDRAWAL_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
|
|
@@ -9,10 +10,13 @@ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d
|
|
|
9
10
|
* with the index of the originating call.
|
|
10
11
|
*
|
|
11
12
|
* **Supported event types:** ERC20 `Transfer(from, to, amount)` and WETH9
|
|
12
|
-
* `Deposit(to, amount)` / `Withdrawal(from, amount)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* `Deposit(to, amount)` / `Withdrawal(from, amount)` from the registered
|
|
14
|
+
* wrapped-native token. Known chains without a wrapped-native token ignore
|
|
15
|
+
* WETH9 events and skip wrapped-native deduplication; chains without registry
|
|
16
|
+
* metadata retain legacy signature-based parsing. On enabled paths, paired
|
|
17
|
+
* mint/burn Transfer events are deduplicated. ERC721 and ERC1155 transfer
|
|
18
|
+
* events are **not** parsed — consumers with NFT flows will see an incomplete
|
|
19
|
+
* transfer list.
|
|
16
20
|
*
|
|
17
21
|
* **Native ETH (`eth_simulateV1` + `traceTransfers`).** When the backend runs
|
|
18
22
|
* `eth_simulateV1` with `traceTransfers` enabled, native-ETH moves — including
|
|
@@ -23,26 +27,36 @@ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d
|
|
|
23
27
|
* backends. Tenderly does not emit these synthetic logs (it derives native ETH
|
|
24
28
|
* separately), so this path is inert there.
|
|
25
29
|
*
|
|
26
|
-
* **WETH9 dedup assumption — canonical atomic emission.** Dedup
|
|
27
|
-
* the same tx: a zero-address
|
|
28
|
-
* `
|
|
29
|
-
* is correct for canonical WETH9,
|
|
30
|
+
* **WETH9 dedup assumption — canonical atomic emission.** Dedup for the
|
|
31
|
+
* registered wrapped-native token is scoped to the same tx: a zero-address
|
|
32
|
+
* `Transfer` is suppressed only if its paired `Deposit`/`Withdrawal` appears
|
|
33
|
+
* in the same `calls[txIdx].logs` slice. This is correct for canonical WETH9,
|
|
34
|
+
* which always emits the `Deposit`/
|
|
30
35
|
* `Withdrawal` and the matching `Transfer(0x0, …)` / `Transfer(…, 0x0)`
|
|
31
36
|
* atomically inside a single call frame. A **non-canonical wrapped-native**
|
|
32
37
|
* that splits these emissions across two txs in the same bundle would leave
|
|
33
38
|
* a phantom zero-address `Transfer` in the parsed output, which can be summed
|
|
34
39
|
* by `assertNoBundlerRetention` and produce a false `BlacklistViolationError`.
|
|
35
|
-
* When
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* onboarding a new chain (see `evm-simulation/CLAUDE.md`).
|
|
40
|
+
* When its zero-address `Transfer` misses same-tx dedup, the parser emits a
|
|
41
|
+
* `warn` so the assumption break is observable before it reaches retention.
|
|
42
|
+
* None of `morpho-sdk`'s currently supported chains require cross-tx dedup;
|
|
43
|
+
* the assumption is rechecked when onboarding a new chain (see
|
|
44
|
+
* `evm-simulation/CLAUDE.md`).
|
|
41
45
|
*
|
|
42
46
|
* **Failure mode:** on a per-log parse failure (malformed topic length,
|
|
43
47
|
* non-hex data), the log is skipped and a `warn` is emitted via the logger.
|
|
44
48
|
*
|
|
45
49
|
* Output is sorted canonically by token, from, to, amount for determinism.
|
|
46
50
|
* `txIdx` is attached but does not influence sort order.
|
|
51
|
+
*
|
|
52
|
+
* @param calls - Per-transaction call results to parse.
|
|
53
|
+
* @param options - Wrapped-native metadata and optional logger. Pass `null` for
|
|
54
|
+
* a known chain without a wrapped-native token; omit it for legacy parsing on
|
|
55
|
+
* an unknown chain.
|
|
56
|
+
* @returns Canonically sorted transfers with their originating transaction index.
|
|
57
|
+
* @internal
|
|
47
58
|
*/
|
|
48
|
-
export declare function parseTransfers(calls: readonly RawCall[],
|
|
59
|
+
export declare function parseTransfers(calls: readonly RawCall[], options?: {
|
|
60
|
+
readonly wNative?: Address | null;
|
|
61
|
+
readonly logger?: SimulationLogger;
|
|
62
|
+
}): Transfer[];
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.DEPOSIT_TOPIC = exports.WITHDRAWAL_TOPIC = exports.TRANSFER_TOPIC = void 0;
|
|
4
4
|
exports.parseTransfers = parseTransfers;
|
|
5
5
|
const viem_1 = require("viem");
|
|
6
|
+
const asset_changes_js_1 = require("../asset-changes.js");
|
|
6
7
|
// keccak256("Transfer(address,address,uint256)") — ERC-20 transfer event
|
|
7
8
|
exports.TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
|
|
8
9
|
// keccak256("Withdrawal(address,uint256)") — WETH9 unwrap event
|
|
@@ -18,10 +19,13 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
18
19
|
* with the index of the originating call.
|
|
19
20
|
*
|
|
20
21
|
* **Supported event types:** ERC20 `Transfer(from, to, amount)` and WETH9
|
|
21
|
-
* `Deposit(to, amount)` / `Withdrawal(from, amount)
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `Deposit(to, amount)` / `Withdrawal(from, amount)` from the registered
|
|
23
|
+
* wrapped-native token. Known chains without a wrapped-native token ignore
|
|
24
|
+
* WETH9 events and skip wrapped-native deduplication; chains without registry
|
|
25
|
+
* metadata retain legacy signature-based parsing. On enabled paths, paired
|
|
26
|
+
* mint/burn Transfer events are deduplicated. ERC721 and ERC1155 transfer
|
|
27
|
+
* events are **not** parsed — consumers with NFT flows will see an incomplete
|
|
28
|
+
* transfer list.
|
|
25
29
|
*
|
|
26
30
|
* **Native ETH (`eth_simulateV1` + `traceTransfers`).** When the backend runs
|
|
27
31
|
* `eth_simulateV1` with `traceTransfers` enabled, native-ETH moves — including
|
|
@@ -32,35 +36,41 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
32
36
|
* backends. Tenderly does not emit these synthetic logs (it derives native ETH
|
|
33
37
|
* separately), so this path is inert there.
|
|
34
38
|
*
|
|
35
|
-
* **WETH9 dedup assumption — canonical atomic emission.** Dedup
|
|
36
|
-
* the same tx: a zero-address
|
|
37
|
-
* `
|
|
38
|
-
* is correct for canonical WETH9,
|
|
39
|
+
* **WETH9 dedup assumption — canonical atomic emission.** Dedup for the
|
|
40
|
+
* registered wrapped-native token is scoped to the same tx: a zero-address
|
|
41
|
+
* `Transfer` is suppressed only if its paired `Deposit`/`Withdrawal` appears
|
|
42
|
+
* in the same `calls[txIdx].logs` slice. This is correct for canonical WETH9,
|
|
43
|
+
* which always emits the `Deposit`/
|
|
39
44
|
* `Withdrawal` and the matching `Transfer(0x0, …)` / `Transfer(…, 0x0)`
|
|
40
45
|
* atomically inside a single call frame. A **non-canonical wrapped-native**
|
|
41
46
|
* that splits these emissions across two txs in the same bundle would leave
|
|
42
47
|
* a phantom zero-address `Transfer` in the parsed output, which can be summed
|
|
43
48
|
* by `assertNoBundlerRetention` and produce a false `BlacklistViolationError`.
|
|
44
|
-
* When
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* onboarding a new chain (see `evm-simulation/CLAUDE.md`).
|
|
49
|
+
* When its zero-address `Transfer` misses same-tx dedup, the parser emits a
|
|
50
|
+
* `warn` so the assumption break is observable before it reaches retention.
|
|
51
|
+
* None of `morpho-sdk`'s currently supported chains require cross-tx dedup;
|
|
52
|
+
* the assumption is rechecked when onboarding a new chain (see
|
|
53
|
+
* `evm-simulation/CLAUDE.md`).
|
|
50
54
|
*
|
|
51
55
|
* **Failure mode:** on a per-log parse failure (malformed topic length,
|
|
52
56
|
* non-hex data), the log is skipped and a `warn` is emitted via the logger.
|
|
53
57
|
*
|
|
54
58
|
* Output is sorted canonically by token, from, to, amount for determinism.
|
|
55
59
|
* `txIdx` is attached but does not influence sort order.
|
|
60
|
+
*
|
|
61
|
+
* @param calls - Per-transaction call results to parse.
|
|
62
|
+
* @param options - Wrapped-native metadata and optional logger. Pass `null` for
|
|
63
|
+
* a known chain without a wrapped-native token; omit it for legacy parsing on
|
|
64
|
+
* an unknown chain.
|
|
65
|
+
* @returns Canonically sorted transfers with their originating transaction index.
|
|
66
|
+
* @internal
|
|
56
67
|
*/
|
|
57
|
-
function parseTransfers(calls,
|
|
68
|
+
function parseTransfers(calls, options = {}) {
|
|
58
69
|
const transfers = [];
|
|
59
|
-
|
|
60
|
-
// wnative-shaped. If a zero-address `Transfer` for one of these tokens
|
|
61
|
-
// misses same-tx dedup, the contract is likely emitting non-canonically
|
|
62
|
-
// (split across txs) and a phantom transfer would leak into retention.
|
|
70
|
+
const { logger, wNative } = options;
|
|
63
71
|
const wnativeShapedTokens = collectWnativeShapedTokens(calls);
|
|
72
|
+
const acceptsWnativeEvent = (address) => wNative === undefined ||
|
|
73
|
+
(wNative !== null && (0, viem_1.isAddressEqual)(address, wNative));
|
|
64
74
|
for (let txIdx = 0; txIdx < calls.length; txIdx++) {
|
|
65
75
|
const logs = calls[txIdx].logs;
|
|
66
76
|
for (const log of logs) {
|
|
@@ -70,6 +80,8 @@ function parseTransfers(calls, logger) {
|
|
|
70
80
|
continue;
|
|
71
81
|
switch (topic0) {
|
|
72
82
|
case exports.WITHDRAWAL_TOPIC: {
|
|
83
|
+
if (!acceptsWnativeEvent(log.address))
|
|
84
|
+
continue;
|
|
73
85
|
const fromTopic = log.topics[1];
|
|
74
86
|
if (!isTopicHex(fromTopic) || !isUint256Hex(log.data)) {
|
|
75
87
|
warnMalformed(logger, log, "WETH9 Withdrawal: bad topic[1] or data length");
|
|
@@ -85,6 +97,8 @@ function parseTransfers(calls, logger) {
|
|
|
85
97
|
continue;
|
|
86
98
|
}
|
|
87
99
|
case exports.DEPOSIT_TOPIC: {
|
|
100
|
+
if (!acceptsWnativeEvent(log.address))
|
|
101
|
+
continue;
|
|
88
102
|
const toTopic = log.topics[1];
|
|
89
103
|
if (!isTopicHex(toTopic) || !isUint256Hex(log.data)) {
|
|
90
104
|
warnMalformed(logger, log, "WETH9 Deposit: bad topic[1] or data length");
|
|
@@ -114,34 +128,36 @@ function parseTransfers(calls, logger) {
|
|
|
114
128
|
}
|
|
115
129
|
// WETH9 unwrap dedup: Transfer to zero paired with a Withdrawal of
|
|
116
130
|
// equal amount in the SAME tx.
|
|
117
|
-
if (toTopic === viem_1.zeroHash) {
|
|
131
|
+
if (toTopic === viem_1.zeroHash && acceptsWnativeEvent(log.address)) {
|
|
118
132
|
const paired = logs.some((other) => other.topics[0] === exports.WITHDRAWAL_TOPIC &&
|
|
119
|
-
other.address
|
|
133
|
+
(0, viem_1.isAddressEqual)(other.address, log.address) &&
|
|
120
134
|
other.data === log.data &&
|
|
121
135
|
other.topics.length === 2 &&
|
|
122
136
|
other.topics[1] === fromTopic);
|
|
123
137
|
if (paired)
|
|
124
138
|
continue;
|
|
125
|
-
if (
|
|
139
|
+
if (wNative !== undefined ||
|
|
140
|
+
wnativeShapedTokens.has(log.address.toLowerCase())) {
|
|
126
141
|
warnNonCanonicalWnative(logger, log, "burn", txIdx);
|
|
127
142
|
}
|
|
128
143
|
}
|
|
129
144
|
// WETH9 wrap dedup: Transfer from zero paired with a Deposit of
|
|
130
145
|
// equal amount in the SAME tx.
|
|
131
|
-
if (fromTopic === viem_1.zeroHash) {
|
|
146
|
+
if (fromTopic === viem_1.zeroHash && acceptsWnativeEvent(log.address)) {
|
|
132
147
|
const paired = logs.some((other) => other.topics[0] === exports.DEPOSIT_TOPIC &&
|
|
133
|
-
other.address
|
|
148
|
+
(0, viem_1.isAddressEqual)(other.address, log.address) &&
|
|
134
149
|
other.data === log.data &&
|
|
135
150
|
other.topics.length === 2 &&
|
|
136
151
|
other.topics[1] === toTopic);
|
|
137
152
|
if (paired)
|
|
138
153
|
continue;
|
|
139
|
-
if (
|
|
154
|
+
if (wNative !== undefined ||
|
|
155
|
+
wnativeShapedTokens.has(log.address.toLowerCase())) {
|
|
140
156
|
warnNonCanonicalWnative(logger, log, "mint", txIdx);
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
transfers.push({
|
|
144
|
-
token:
|
|
160
|
+
token: (0, asset_changes_js_1.normalizeAssetToken)(log.address),
|
|
145
161
|
from: (0, viem_1.getAddress)(`0x${fromTopic.slice(26)}`),
|
|
146
162
|
to: (0, viem_1.getAddress)(`0x${toTopic.slice(26)}`),
|
|
147
163
|
amount: BigInt(log.data),
|
|
@@ -162,21 +178,6 @@ function parseTransfers(calls, logger) {
|
|
|
162
178
|
}
|
|
163
179
|
return sortTransfers(transfers);
|
|
164
180
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Normalize a `Transfer` log's emitting address into a transfer `token`.
|
|
167
|
-
*
|
|
168
|
-
* `eth_simulateV1` with `traceTransfers` enabled synthesizes native-ETH moves
|
|
169
|
-
* (including internal calls) as `Transfer` events emitted from the native
|
|
170
|
-
* sentinel `0xeee…eee`. The rest of the SDK keys native ETH by viem's
|
|
171
|
-
* `ethAddress` constant, so we collapse the sentinel to that exact value rather
|
|
172
|
-
* than the checksummed form `getAddress` would produce — keeping native deltas
|
|
173
|
-
* on a single map key across backends. Real ERC20 tokens are checksummed.
|
|
174
|
-
*/
|
|
175
|
-
function normalizeTransferToken(address) {
|
|
176
|
-
return address.toLowerCase() === viem_1.ethAddress
|
|
177
|
-
? viem_1.ethAddress
|
|
178
|
-
: (0, viem_1.getAddress)(address);
|
|
179
|
-
}
|
|
180
181
|
function isTopicHex(value) {
|
|
181
182
|
return typeof value === "string" && value.length === TOPIC_HEX_LENGTH;
|
|
182
183
|
}
|
|
@@ -191,24 +192,18 @@ function warnMalformed(logger, log, reason) {
|
|
|
191
192
|
reason,
|
|
192
193
|
});
|
|
193
194
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Collect token addresses (lowercased) that emit `Deposit` or `Withdrawal`
|
|
196
|
-
* anywhere in the bundle. Used to detect when a zero-address `Transfer` that
|
|
197
|
-
* misses same-tx dedup is on a wnative-shaped contract — a strong signal
|
|
198
|
-
* that the contract is emitting non-canonically and a phantom transfer is
|
|
199
|
-
* about to leak into bundler retention.
|
|
200
|
-
*/
|
|
195
|
+
/** Collect contracts that emit WETH9-shaped events anywhere in the bundle. */
|
|
201
196
|
function collectWnativeShapedTokens(calls) {
|
|
202
|
-
const
|
|
203
|
-
for (const
|
|
204
|
-
for (const
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
|
|
197
|
+
const tokens = new Set();
|
|
198
|
+
for (const call of calls) {
|
|
199
|
+
for (const log of call.logs) {
|
|
200
|
+
const topic0 = log.topics[0];
|
|
201
|
+
if (topic0 === exports.WITHDRAWAL_TOPIC || topic0 === exports.DEPOSIT_TOPIC) {
|
|
202
|
+
tokens.add(log.address.toLowerCase());
|
|
208
203
|
}
|
|
209
204
|
}
|
|
210
205
|
}
|
|
211
|
-
return
|
|
206
|
+
return tokens;
|
|
212
207
|
}
|
|
213
208
|
// biome-ignore lint/complexity/useMaxParams: structured warn with full context
|
|
214
209
|
function warnNonCanonicalWnative(logger, log, kind, txIdx) {
|
|
@@ -6,15 +6,17 @@ interface AssertNoBundlerRetentionParams {
|
|
|
6
6
|
logger?: SimulationLogger;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Assert that no value is retained by
|
|
9
|
+
* Assert that no value is retained by restricted contract addresses — the
|
|
10
|
+
* `bundler3` executor and adapters, plus the standalone `bundles` periphery
|
|
11
|
+
* contracts (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`).
|
|
10
12
|
*
|
|
11
|
-
* Uses net flow (inbound minus outbound) per (
|
|
12
|
-
*
|
|
13
|
-
* vault), so gross inbound would fire false positives. We only block
|
|
14
|
-
* net flow above `DUST_THRESHOLD` — value stuck in a
|
|
15
|
-
* net flow means the simulated bundle sweeps a pre-existing or
|
|
16
|
-
*
|
|
17
|
-
* so it is warned instead of raising a blacklist violation.
|
|
13
|
+
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
14
|
+
* These contracts legitimately receive tokens as an intermediary (user →
|
|
15
|
+
* bundler → vault), so gross inbound would fire false positives. We only block
|
|
16
|
+
* positive net flow above `DUST_THRESHOLD` — value stuck in a restricted
|
|
17
|
+
* address. Negative net flow means the simulated bundle sweeps a pre-existing or
|
|
18
|
+
* state-overridden balance. That is useful telemetry, but not current-bundle
|
|
19
|
+
* retention, so it is warned instead of raising a blacklist violation.
|
|
18
20
|
*
|
|
19
21
|
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
20
22
|
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
@@ -30,6 +32,22 @@ interface AssertNoBundlerRetentionParams {
|
|
|
30
32
|
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
31
33
|
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
32
34
|
* fallback for bundler addresses that carry no native `assetChanges` entry.
|
|
35
|
+
*
|
|
36
|
+
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
37
|
+
* package's public API (only re-exported from the internal pipeline barrel).
|
|
38
|
+
* @param params.chainId - Chain whose blue-sdk `bundler3` and `bundles`
|
|
39
|
+
* registries define the restricted address set. Chains cataloging neither are
|
|
40
|
+
* skipped (a `logger.warn` records the skip).
|
|
41
|
+
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
42
|
+
* `eth_simulateV1` synthetic native sentinel) scanned for net retention.
|
|
43
|
+
* @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
|
|
44
|
+
* source of truth for native value stuck in a restricted contract.
|
|
45
|
+
* @param params.logger - Optional logger. Receives retention-check skip warnings
|
|
46
|
+
* and pre-existing-balance sweep telemetry (net-negative flow).
|
|
47
|
+
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
48
|
+
* above `DUST_THRESHOLD`; otherwise throws.
|
|
49
|
+
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
50
|
+
* `bundler3` or `bundles` contract exceeds `DUST_THRESHOLD` for any token.
|
|
33
51
|
*/
|
|
34
52
|
export declare function assertNoBundlerRetention(params: AssertNoBundlerRetentionParams): void;
|
|
35
53
|
export {};
|
|
@@ -15,43 +15,18 @@ const errors_js_1 = require("../../errors.js");
|
|
|
15
15
|
* acceptable retention amount.
|
|
16
16
|
*/
|
|
17
17
|
const DUST_THRESHOLD = 100n;
|
|
18
|
-
function getBundlerAddresses(chainId, logger) {
|
|
19
|
-
try {
|
|
20
|
-
const addresses = (0, blue_sdk_1.getChainAddresses)(chainId);
|
|
21
|
-
if (!addresses.bundler3) {
|
|
22
|
-
// blue-sdk knows the chain but didn't catalog a bundler3 for it.
|
|
23
|
-
// Treat the same as UnsupportedChainIdError — retention check skipped.
|
|
24
|
-
logger?.warn("Chain known to blue-sdk but has no bundler3 config, retention check skipped", {
|
|
25
|
-
chainId,
|
|
26
|
-
});
|
|
27
|
-
return new Set();
|
|
28
|
-
}
|
|
29
|
-
return new Set(Object.values(addresses.bundler3)
|
|
30
|
-
.filter(morpho_ts_1.isDefined)
|
|
31
|
-
.map((addr) => (0, viem_1.getAddress)(addr)));
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
if (error instanceof blue_sdk_1.UnsupportedChainIdError) {
|
|
35
|
-
// Loud warn: this disables a "never bypassable" check for the chain.
|
|
36
|
-
// Consumers relying on the guarantee must handle this signal.
|
|
37
|
-
logger?.warn("Chain not supported by blue-sdk, retention check skipped", {
|
|
38
|
-
chainId,
|
|
39
|
-
});
|
|
40
|
-
return new Set();
|
|
41
|
-
}
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
18
|
/**
|
|
46
|
-
* Assert that no value is retained by
|
|
19
|
+
* Assert that no value is retained by restricted contract addresses — the
|
|
20
|
+
* `bundler3` executor and adapters, plus the standalone `bundles` periphery
|
|
21
|
+
* contracts (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`).
|
|
47
22
|
*
|
|
48
|
-
* Uses net flow (inbound minus outbound) per (
|
|
49
|
-
*
|
|
50
|
-
* vault), so gross inbound would fire false positives. We only block
|
|
51
|
-
* net flow above `DUST_THRESHOLD` — value stuck in a
|
|
52
|
-
* net flow means the simulated bundle sweeps a pre-existing or
|
|
53
|
-
*
|
|
54
|
-
* so it is warned instead of raising a blacklist violation.
|
|
23
|
+
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
24
|
+
* These contracts legitimately receive tokens as an intermediary (user →
|
|
25
|
+
* bundler → vault), so gross inbound would fire false positives. We only block
|
|
26
|
+
* positive net flow above `DUST_THRESHOLD` — value stuck in a restricted
|
|
27
|
+
* address. Negative net flow means the simulated bundle sweeps a pre-existing or
|
|
28
|
+
* state-overridden balance. That is useful telemetry, but not current-bundle
|
|
29
|
+
* retention, so it is warned instead of raising a blacklist violation.
|
|
55
30
|
*
|
|
56
31
|
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
57
32
|
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
@@ -67,13 +42,61 @@ function getBundlerAddresses(chainId, logger) {
|
|
|
67
42
|
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
68
43
|
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
69
44
|
* fallback for bundler addresses that carry no native `assetChanges` entry.
|
|
45
|
+
*
|
|
46
|
+
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
47
|
+
* package's public API (only re-exported from the internal pipeline barrel).
|
|
48
|
+
* @param params.chainId - Chain whose blue-sdk `bundler3` and `bundles`
|
|
49
|
+
* registries define the restricted address set. Chains cataloging neither are
|
|
50
|
+
* skipped (a `logger.warn` records the skip).
|
|
51
|
+
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
52
|
+
* `eth_simulateV1` synthetic native sentinel) scanned for net retention.
|
|
53
|
+
* @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
|
|
54
|
+
* source of truth for native value stuck in a restricted contract.
|
|
55
|
+
* @param params.logger - Optional logger. Receives retention-check skip warnings
|
|
56
|
+
* and pre-existing-balance sweep telemetry (net-negative flow).
|
|
57
|
+
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
58
|
+
* above `DUST_THRESHOLD`; otherwise throws.
|
|
59
|
+
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
60
|
+
* `bundler3` or `bundles` contract exceeds `DUST_THRESHOLD` for any token.
|
|
70
61
|
*/
|
|
71
62
|
function assertNoBundlerRetention(params) {
|
|
72
63
|
const { chainId, transfers, assetChanges, logger } = params;
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
// Restricted address set: the full `bundler3` sub-registry (executor + every
|
|
65
|
+
// adapter) plus the standalone `bundles` periphery contracts
|
|
66
|
+
// (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`). Both families are
|
|
67
|
+
// transient intermediaries that route user value and must never retain it, so
|
|
68
|
+
// both are guarded identically.
|
|
69
|
+
let addresses;
|
|
70
|
+
try {
|
|
71
|
+
addresses = (0, blue_sdk_1.getChainAddresses)(chainId);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof blue_sdk_1.UnsupportedChainIdError) {
|
|
75
|
+
// Loud warn: this disables a "never bypassable" check for the chain.
|
|
76
|
+
// Consumers relying on the guarantee must handle this signal.
|
|
77
|
+
logger?.warn("Chain not supported by blue-sdk, retention check skipped", {
|
|
78
|
+
chainId,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const restrictedAddresses = new Set();
|
|
85
|
+
if (addresses.bundler3) {
|
|
86
|
+
for (const addr of Object.values(addresses.bundler3).filter(morpho_ts_1.isDefined))
|
|
87
|
+
restrictedAddresses.add((0, viem_1.getAddress)(addr));
|
|
88
|
+
}
|
|
89
|
+
if (addresses.bundles) {
|
|
90
|
+
for (const addr of Object.values(addresses.bundles).filter(morpho_ts_1.isDefined))
|
|
91
|
+
restrictedAddresses.add((0, viem_1.getAddress)(addr));
|
|
92
|
+
}
|
|
93
|
+
if (restrictedAddresses.size === 0) {
|
|
94
|
+
// blue-sdk knows the chain but cataloged neither bundler3 nor bundles for
|
|
95
|
+
// it. Treat the same as UnsupportedChainIdError — retention check skipped.
|
|
96
|
+
logger?.warn("Chain known to blue-sdk but has no bundler3 or bundles config, retention check skipped", { chainId });
|
|
75
97
|
return;
|
|
76
|
-
|
|
98
|
+
}
|
|
99
|
+
// Map keyed by (address, token) → structured entry. Avoids string parse-back.
|
|
77
100
|
const flow = new Map();
|
|
78
101
|
// biome-ignore lint/complexity/useMaxParams: TODO refactor to ≤2 params
|
|
79
102
|
const recordFlow = (address, token, delta) => {
|
|
@@ -91,7 +114,7 @@ function assertNoBundlerRetention(params) {
|
|
|
91
114
|
// does not re-add the same native move on `eth_simulateV1`.
|
|
92
115
|
const nativeFromAssetChanges = new Set();
|
|
93
116
|
for (const { account, changes } of assetChanges) {
|
|
94
|
-
if (!
|
|
117
|
+
if (!restrictedAddresses.has(account))
|
|
95
118
|
continue;
|
|
96
119
|
for (const change of changes) {
|
|
97
120
|
if (change.token !== viem_1.ethAddress)
|
|
@@ -106,15 +129,15 @@ function assertNoBundlerRetention(params) {
|
|
|
106
129
|
// of the `assetChanges` value already recorded for that address.
|
|
107
130
|
const usesAssetChangeNative = (t, addr) => t.token === viem_1.ethAddress && nativeFromAssetChanges.has(addr.toLowerCase());
|
|
108
131
|
for (const t of transfers) {
|
|
109
|
-
if (
|
|
132
|
+
if (restrictedAddresses.has(t.to) && !usesAssetChangeNative(t, t.to))
|
|
110
133
|
recordFlow(t.to, t.token, t.amount);
|
|
111
|
-
if (
|
|
134
|
+
if (restrictedAddresses.has(t.from) && !usesAssetChangeNative(t, t.from))
|
|
112
135
|
recordFlow(t.from, t.token, -t.amount);
|
|
113
136
|
}
|
|
114
137
|
const entries = [...flow.values()];
|
|
115
138
|
const drained = entries.filter((e) => e.netChange < -DUST_THRESHOLD);
|
|
116
139
|
if (drained.length > 0) {
|
|
117
|
-
logger?.warn("Simulation detected pre-existing
|
|
140
|
+
logger?.warn("Simulation detected pre-existing balance being swept from a restricted bundler or bundles contract", {
|
|
118
141
|
changes: drained.map((e) => ({
|
|
119
142
|
address: e.address,
|
|
120
143
|
token: e.token,
|
|
@@ -124,7 +147,7 @@ function assertNoBundlerRetention(params) {
|
|
|
124
147
|
}
|
|
125
148
|
const retained = entries.filter((e) => e.netChange > DUST_THRESHOLD);
|
|
126
149
|
if (retained.length > 0) {
|
|
127
|
-
throw new errors_js_1.BlacklistViolationError("Simulation detected asset transfers retained by restricted bundler contracts", retained.map((e) => ({
|
|
150
|
+
throw new errors_js_1.BlacklistViolationError("Simulation detected asset transfers retained by restricted bundler or bundles contracts", retained.map((e) => ({
|
|
128
151
|
address: e.address,
|
|
129
152
|
token: e.token,
|
|
130
153
|
netRetained: e.netChange.toString(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlockTag } from "viem";
|
|
1
|
+
import type { Address, BlockTag } from "viem";
|
|
2
2
|
import type { RawSimulationResult, SimulationConfig, SimulationTransaction } from "../../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Stage 4 of the simulate() pipeline.
|
|
@@ -18,4 +18,5 @@ export declare function executeSimulation(params: {
|
|
|
18
18
|
chainId: number;
|
|
19
19
|
transactions: SimulationTransaction[];
|
|
20
20
|
blockNumber?: bigint | BlockTag;
|
|
21
|
+
wNative?: Address | null;
|
|
21
22
|
}): Promise<RawSimulationResult>;
|
|
@@ -32,7 +32,7 @@ const FALLBACK_MIN_BUDGET_MS = 1500;
|
|
|
32
32
|
* - If only `eth_simulateV1` is configured, it gets the full timeout.
|
|
33
33
|
*/
|
|
34
34
|
async function executeSimulation(params) {
|
|
35
|
-
const { config, chainId, transactions, blockNumber } = params;
|
|
35
|
+
const { config, chainId, transactions, blockNumber, wNative } = params;
|
|
36
36
|
const chain = (0, resolve_chain_js_1.resolveChain)(config, chainId);
|
|
37
37
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
38
38
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -62,6 +62,7 @@ async function executeSimulation(params) {
|
|
|
62
62
|
chainId,
|
|
63
63
|
transactions,
|
|
64
64
|
blockNumber,
|
|
65
|
+
wNative,
|
|
65
66
|
signal: AbortSignal.timeout(fallbackBudget),
|
|
66
67
|
});
|
|
67
68
|
}
|
|
@@ -75,6 +76,7 @@ async function executeSimulation(params) {
|
|
|
75
76
|
chainId,
|
|
76
77
|
transactions,
|
|
77
78
|
blockNumber,
|
|
79
|
+
wNative,
|
|
78
80
|
signal: AbortSignal.timeout(timeoutMs),
|
|
79
81
|
});
|
|
80
82
|
}
|