@morpho-org/evm-simulation 4.1.10 → 4.2.0-next.2
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 +1 -1
- package/lib/cjs/simulate/asset-changes.js +1 -1
- package/lib/cjs/simulate/parsing/transfers.d.ts +5 -1
- package/lib/cjs/simulate/parsing/transfers.js +16 -6
- package/lib/cjs/simulate/pipeline/{bundler-retention.d.ts → bundles-retention.d.ts} +19 -17
- package/lib/cjs/simulate/pipeline/{bundler-retention.js → bundles-retention.js} +37 -39
- package/lib/cjs/simulate/pipeline/index.d.ts +1 -1
- package/lib/cjs/simulate/pipeline/index.js +3 -3
- package/lib/cjs/simulate/simulate.d.ts +3 -3
- package/lib/cjs/simulate/simulate.js +4 -4
- package/lib/esm/errors.d.ts +1 -1
- package/lib/esm/errors.js +1 -1
- package/lib/esm/simulate/asset-changes.d.ts +1 -1
- package/lib/esm/simulate/asset-changes.js +1 -1
- package/lib/esm/simulate/parsing/transfers.d.ts +5 -1
- package/lib/esm/simulate/parsing/transfers.js +16 -6
- package/lib/esm/simulate/pipeline/{bundler-retention.d.ts → bundles-retention.d.ts} +19 -17
- package/lib/esm/simulate/pipeline/{bundler-retention.js → bundles-retention.js} +36 -38
- package/lib/esm/simulate/pipeline/index.d.ts +1 -1
- package/lib/esm/simulate/pipeline/index.js +1 -1
- package/lib/esm/simulate/simulate.d.ts +3 -3
- package/lib/esm/simulate/simulate.js +5 -5
- package/package.json +3 -6
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 restricted
|
|
22
|
+
/** Funds would flow to restricted standalone 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 restricted
|
|
27
|
+
/** Funds would flow to restricted standalone bundles contract addresses. Never bypassable. */
|
|
28
28
|
class BlacklistViolationError extends SimulationPackageError {
|
|
29
29
|
assetChanges;
|
|
30
30
|
code = "BLACKLIST_ERROR";
|
|
@@ -13,7 +13,7 @@ export interface AssetChangeEntry {
|
|
|
13
13
|
* any real token address. `eth_simulateV1` synthesizes native moves from the
|
|
14
14
|
* sentinel `0xeee…eee`, and Tenderly may echo it — in checksummed or any other
|
|
15
15
|
* case — as an asset change's `contractAddress`. Native ETH is keyed by the exact
|
|
16
|
-
* `ethAddress` constant here and in the
|
|
16
|
+
* `ethAddress` constant here and in the bundles-retention guard, so a checksummed
|
|
17
17
|
* sentinel would land on a separate map key and silently escape retention checks.
|
|
18
18
|
* This is the single source of truth for token normalization shared by the
|
|
19
19
|
* transfer-log parser and the Tenderly asset-change mapper.
|
|
@@ -8,7 +8,7 @@ const viem_1 = require("viem");
|
|
|
8
8
|
* any real token address. `eth_simulateV1` synthesizes native moves from the
|
|
9
9
|
* sentinel `0xeee…eee`, and Tenderly may echo it — in checksummed or any other
|
|
10
10
|
* case — as an asset change's `contractAddress`. Native ETH is keyed by the exact
|
|
11
|
-
* `ethAddress` constant here and in the
|
|
11
|
+
* `ethAddress` constant here and in the bundles-retention guard, so a checksummed
|
|
12
12
|
* sentinel would land on a separate map key and silently escape retention checks.
|
|
13
13
|
* This is the single source of truth for token normalization shared by the
|
|
14
14
|
* transfer-log parser and the Tenderly asset-change mapper.
|
|
@@ -36,13 +36,17 @@ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d
|
|
|
36
36
|
* atomically inside a single call frame. A **non-canonical wrapped-native**
|
|
37
37
|
* that splits these emissions across two txs in the same bundle would leave
|
|
38
38
|
* a phantom zero-address `Transfer` in the parsed output, which can be summed
|
|
39
|
-
* by `
|
|
39
|
+
* by `assertNoBundlesRetention` and produce a false `BlacklistViolationError`.
|
|
40
40
|
* When its zero-address `Transfer` misses same-tx dedup, the parser emits a
|
|
41
41
|
* `warn` so the assumption break is observable before it reaches retention.
|
|
42
42
|
* None of `morpho-sdk`'s currently supported chains require cross-tx dedup;
|
|
43
43
|
* the assumption is rechecked when onboarding a new chain (see
|
|
44
44
|
* `evm-simulation/CLAUDE.md`).
|
|
45
45
|
*
|
|
46
|
+
* **Hex casing.** JSON-RPC `DATA` is case-insensitive, so `topics` and `data`
|
|
47
|
+
* are lowercased before signature dispatch and pair matching; a backend that
|
|
48
|
+
* echoes mixed-case hex cannot drop a transfer from the parsed output.
|
|
49
|
+
*
|
|
46
50
|
* **Failure mode:** on a per-log parse failure (malformed topic length,
|
|
47
51
|
* non-hex data), the log is skipped and a `warn` is emitted via the logger.
|
|
48
52
|
*
|
|
@@ -45,13 +45,17 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
45
45
|
* atomically inside a single call frame. A **non-canonical wrapped-native**
|
|
46
46
|
* that splits these emissions across two txs in the same bundle would leave
|
|
47
47
|
* a phantom zero-address `Transfer` in the parsed output, which can be summed
|
|
48
|
-
* by `
|
|
48
|
+
* by `assertNoBundlesRetention` and produce a false `BlacklistViolationError`.
|
|
49
49
|
* When its zero-address `Transfer` misses same-tx dedup, the parser emits a
|
|
50
50
|
* `warn` so the assumption break is observable before it reaches retention.
|
|
51
51
|
* None of `morpho-sdk`'s currently supported chains require cross-tx dedup;
|
|
52
52
|
* the assumption is rechecked when onboarding a new chain (see
|
|
53
53
|
* `evm-simulation/CLAUDE.md`).
|
|
54
54
|
*
|
|
55
|
+
* **Hex casing.** JSON-RPC `DATA` is case-insensitive, so `topics` and `data`
|
|
56
|
+
* are lowercased before signature dispatch and pair matching; a backend that
|
|
57
|
+
* echoes mixed-case hex cannot drop a transfer from the parsed output.
|
|
58
|
+
*
|
|
55
59
|
* **Failure mode:** on a per-log parse failure (malformed topic length,
|
|
56
60
|
* non-hex data), the log is skipped and a `warn` is emitted via the logger.
|
|
57
61
|
*
|
|
@@ -68,11 +72,17 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
68
72
|
function parseTransfers(calls, options = {}) {
|
|
69
73
|
const transfers = [];
|
|
70
74
|
const { logger, wNative } = options;
|
|
71
|
-
|
|
75
|
+
// Lowercase `topics`/`data` so signature dispatch and pair matching are case-insensitive.
|
|
76
|
+
const normalizedCalls = calls.map((call) => call.logs.map((log) => ({
|
|
77
|
+
...log,
|
|
78
|
+
topics: log.topics.map((topic) => topic.toLowerCase()),
|
|
79
|
+
data: log.data.toLowerCase(),
|
|
80
|
+
})));
|
|
81
|
+
const wnativeShapedTokens = collectWnativeShapedTokens(normalizedCalls);
|
|
72
82
|
const acceptsWnativeEvent = (address) => wNative === undefined ||
|
|
73
83
|
(wNative !== null && (0, viem_1.isAddressEqual)(address, wNative));
|
|
74
|
-
for (let txIdx = 0; txIdx <
|
|
75
|
-
const logs =
|
|
84
|
+
for (let txIdx = 0; txIdx < normalizedCalls.length; txIdx++) {
|
|
85
|
+
const logs = normalizedCalls[txIdx];
|
|
76
86
|
for (const log of logs) {
|
|
77
87
|
try {
|
|
78
88
|
const topic0 = log.topics[0];
|
|
@@ -195,8 +205,8 @@ function warnMalformed(logger, log, reason) {
|
|
|
195
205
|
/** Collect contracts that emit WETH9-shaped events anywhere in the bundle. */
|
|
196
206
|
function collectWnativeShapedTokens(calls) {
|
|
197
207
|
const tokens = new Set();
|
|
198
|
-
for (const
|
|
199
|
-
for (const log of
|
|
208
|
+
for (const logs of calls) {
|
|
209
|
+
for (const log of logs) {
|
|
200
210
|
const topic0 = log.topics[0];
|
|
201
211
|
if (topic0 === exports.WITHDRAWAL_TOPIC || topic0 === exports.DEPOSIT_TOPIC) {
|
|
202
212
|
tokens.add(log.address.toLowerCase());
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import type { AccountAssetChanges, SimulationLogger, Transfer } from "../../types.js";
|
|
2
|
-
interface
|
|
2
|
+
interface AssertNoBundlesRetentionParams {
|
|
3
3
|
chainId: number;
|
|
4
4
|
transfers: Transfer[];
|
|
5
5
|
assetChanges: readonly AccountAssetChanges[];
|
|
6
6
|
logger?: SimulationLogger;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Assert that no value is retained by
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
9
|
+
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
10
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
11
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
12
12
|
*
|
|
13
13
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
14
14
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
15
|
-
*
|
|
16
|
-
* positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
17
|
-
* address. Negative net flow means the simulated bundle sweeps a
|
|
18
|
-
* state-overridden balance. That is useful telemetry, but not
|
|
19
|
-
* retention, so it is warned instead of raising a blacklist
|
|
15
|
+
* bundles contract → vault), so gross inbound would fire false positives. We
|
|
16
|
+
* only block positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
17
|
+
* restricted address. Negative net flow means the simulated bundle sweeps a
|
|
18
|
+
* pre-existing or state-overridden balance. That is useful telemetry, but not
|
|
19
|
+
* current-bundle retention, so it is warned instead of raising a blacklist
|
|
20
|
+
* violation.
|
|
20
21
|
*
|
|
21
22
|
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
22
23
|
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
@@ -25,19 +26,19 @@ interface AssertNoBundlerRetentionParams {
|
|
|
25
26
|
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
26
27
|
* truth for native value: Tenderly derives it from its trace, and
|
|
27
28
|
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
28
|
-
* this, native ETH stuck in
|
|
29
|
-
* Tenderly path (Cantina finding 1440).
|
|
29
|
+
* this, native ETH stuck in a bundles contract would pass the guard undetected
|
|
30
|
+
* on the Tenderly path (Cantina finding 1440).
|
|
30
31
|
*
|
|
31
32
|
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
32
33
|
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
33
34
|
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
34
|
-
* fallback for
|
|
35
|
+
* fallback for bundles addresses that carry no native `assetChanges` entry.
|
|
35
36
|
*
|
|
36
37
|
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
37
38
|
* package's public API (only re-exported from the internal pipeline barrel).
|
|
38
|
-
* @param params.chainId - Chain whose blue-sdk `
|
|
39
|
-
*
|
|
40
|
-
* skipped (a `logger.warn` records the skip).
|
|
39
|
+
* @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundles`
|
|
40
|
+
* registry entries define the restricted address set. Chains cataloging
|
|
41
|
+
* neither are skipped (a `logger.warn` records the skip).
|
|
41
42
|
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
42
43
|
* `eth_simulateV1` synthetic native sentinel) scanned for net retention.
|
|
43
44
|
* @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
|
|
@@ -47,7 +48,8 @@ interface AssertNoBundlerRetentionParams {
|
|
|
47
48
|
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
48
49
|
* above `DUST_THRESHOLD`; otherwise throws.
|
|
49
50
|
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
50
|
-
* `
|
|
51
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
52
|
+
* token.
|
|
51
53
|
*/
|
|
52
|
-
export declare function
|
|
54
|
+
export declare function assertNoBundlesRetention(params: AssertNoBundlesRetentionParams): void;
|
|
53
55
|
export {};
|
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.assertNoBundlesRetention = assertNoBundlesRetention;
|
|
4
4
|
const blue_sdk_1 = require("@morpho-org/blue-sdk");
|
|
5
5
|
const morpho_ts_1 = require("@morpho-org/morpho-ts");
|
|
6
6
|
const viem_1 = require("viem");
|
|
7
7
|
const errors_js_1 = require("../../errors.js");
|
|
8
8
|
/**
|
|
9
|
-
* Dust tolerance for
|
|
9
|
+
* Dust tolerance for standalone bundles retention, in raw token units.
|
|
10
10
|
*
|
|
11
11
|
* 100 units is negligible for 18-decimal tokens (~1e-16). For low-decimal
|
|
12
12
|
* tokens (e.g., 2-decimal stablecoins) this represents ~1.00 token — worth
|
|
13
13
|
* keeping in mind. The threshold is intentionally token-agnostic: its purpose
|
|
14
|
-
* is to absorb rounding noise
|
|
15
|
-
*
|
|
14
|
+
* is to absorb routing rounding noise, not to define an acceptable retention
|
|
15
|
+
* amount.
|
|
16
16
|
*/
|
|
17
17
|
const DUST_THRESHOLD = 100n;
|
|
18
18
|
/**
|
|
19
|
-
* Assert that no value is retained by
|
|
20
|
-
* `
|
|
21
|
-
*
|
|
19
|
+
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
20
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
21
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
22
22
|
*
|
|
23
23
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
24
24
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
25
|
-
*
|
|
26
|
-
* positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
27
|
-
* address. Negative net flow means the simulated bundle sweeps a
|
|
28
|
-
* state-overridden balance. That is useful telemetry, but not
|
|
29
|
-
* retention, so it is warned instead of raising a blacklist
|
|
25
|
+
* bundles contract → vault), so gross inbound would fire false positives. We
|
|
26
|
+
* only block positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
27
|
+
* restricted address. Negative net flow means the simulated bundle sweeps a
|
|
28
|
+
* pre-existing or state-overridden balance. That is useful telemetry, but not
|
|
29
|
+
* current-bundle retention, so it is warned instead of raising a blacklist
|
|
30
|
+
* violation.
|
|
30
31
|
*
|
|
31
32
|
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
32
33
|
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
@@ -35,19 +36,19 @@ const DUST_THRESHOLD = 100n;
|
|
|
35
36
|
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
36
37
|
* truth for native value: Tenderly derives it from its trace, and
|
|
37
38
|
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
38
|
-
* this, native ETH stuck in
|
|
39
|
-
* Tenderly path (Cantina finding 1440).
|
|
39
|
+
* this, native ETH stuck in a bundles contract would pass the guard undetected
|
|
40
|
+
* on the Tenderly path (Cantina finding 1440).
|
|
40
41
|
*
|
|
41
42
|
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
42
43
|
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
43
44
|
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
44
|
-
* fallback for
|
|
45
|
+
* fallback for bundles addresses that carry no native `assetChanges` entry.
|
|
45
46
|
*
|
|
46
47
|
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
47
48
|
* package's public API (only re-exported from the internal pipeline barrel).
|
|
48
|
-
* @param params.chainId - Chain whose blue-sdk `
|
|
49
|
-
*
|
|
50
|
-
* skipped (a `logger.warn` records the skip).
|
|
49
|
+
* @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundles`
|
|
50
|
+
* registry entries define the restricted address set. Chains cataloging
|
|
51
|
+
* neither are skipped (a `logger.warn` records the skip).
|
|
51
52
|
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
52
53
|
* `eth_simulateV1` synthetic native sentinel) scanned for net retention.
|
|
53
54
|
* @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
|
|
@@ -57,15 +58,13 @@ const DUST_THRESHOLD = 100n;
|
|
|
57
58
|
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
58
59
|
* above `DUST_THRESHOLD`; otherwise throws.
|
|
59
60
|
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
60
|
-
* `
|
|
61
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
62
|
+
* token.
|
|
61
63
|
*/
|
|
62
|
-
function
|
|
64
|
+
function assertNoBundlesRetention(params) {
|
|
63
65
|
const { chainId, transfers, assetChanges, logger } = params;
|
|
64
|
-
//
|
|
65
|
-
//
|
|
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.
|
|
66
|
+
// Standalone bundles contracts and `MidnightBundlesV1` are transient
|
|
67
|
+
// intermediaries that route user value and must never retain it.
|
|
69
68
|
let addresses;
|
|
70
69
|
try {
|
|
71
70
|
addresses = (0, blue_sdk_1.getChainAddresses)(chainId);
|
|
@@ -82,18 +81,17 @@ function assertNoBundlerRetention(params) {
|
|
|
82
81
|
throw error;
|
|
83
82
|
}
|
|
84
83
|
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
84
|
if (addresses.bundles) {
|
|
90
85
|
for (const addr of Object.values(addresses.bundles).filter(morpho_ts_1.isDefined))
|
|
91
86
|
restrictedAddresses.add((0, viem_1.getAddress)(addr));
|
|
92
87
|
}
|
|
88
|
+
if (addresses.midnightBundles) {
|
|
89
|
+
restrictedAddresses.add((0, viem_1.getAddress)(addresses.midnightBundles));
|
|
90
|
+
}
|
|
93
91
|
if (restrictedAddresses.size === 0) {
|
|
94
|
-
// blue-sdk knows the chain but cataloged
|
|
95
|
-
//
|
|
96
|
-
logger?.warn("Chain known to blue-sdk but has no
|
|
92
|
+
// blue-sdk knows the chain but cataloged no restricted intermediary for it.
|
|
93
|
+
// Treat the same as UnsupportedChainIdError — retention check skipped.
|
|
94
|
+
logger?.warn("Chain known to blue-sdk but has no restricted intermediary config, retention check skipped", { chainId });
|
|
97
95
|
return;
|
|
98
96
|
}
|
|
99
97
|
// Map keyed by (address, token) → structured entry. Avoids string parse-back.
|
|
@@ -110,8 +108,8 @@ function assertNoBundlerRetention(params) {
|
|
|
110
108
|
}
|
|
111
109
|
};
|
|
112
110
|
// Native ETH from `assetChanges` (authoritative, cross-backend). Track which
|
|
113
|
-
//
|
|
114
|
-
// does not re-add the same native move on `eth_simulateV1`.
|
|
111
|
+
// restricted contracts already have a native entry here so the transfer-log
|
|
112
|
+
// pass below does not re-add the same native move on `eth_simulateV1`.
|
|
115
113
|
const nativeFromAssetChanges = new Set();
|
|
116
114
|
for (const { account, changes } of assetChanges) {
|
|
117
115
|
if (!restrictedAddresses.has(account))
|
|
@@ -124,9 +122,9 @@ function assertNoBundlerRetention(params) {
|
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
// ERC20 / WETH9 retention from transfer logs. Native ETH transfers (the
|
|
127
|
-
// `eth_simulateV1` synthetic sentinel) only backfill a
|
|
128
|
-
// is absent from `nativeFromAssetChanges`, so native is never summed on
|
|
129
|
-
// of the `assetChanges` value already recorded for that address.
|
|
125
|
+
// `eth_simulateV1` synthetic sentinel) only backfill a restricted endpoint
|
|
126
|
+
// that is absent from `nativeFromAssetChanges`, so native is never summed on
|
|
127
|
+
// top of the `assetChanges` value already recorded for that address.
|
|
130
128
|
const usesAssetChangeNative = (t, addr) => t.token === viem_1.ethAddress && nativeFromAssetChanges.has(addr.toLowerCase());
|
|
131
129
|
for (const t of transfers) {
|
|
132
130
|
if (restrictedAddresses.has(t.to) && !usesAssetChangeNative(t, t.to))
|
|
@@ -137,7 +135,7 @@ function assertNoBundlerRetention(params) {
|
|
|
137
135
|
const entries = [...flow.values()];
|
|
138
136
|
const drained = entries.filter((e) => e.netChange < -DUST_THRESHOLD);
|
|
139
137
|
if (drained.length > 0) {
|
|
140
|
-
logger?.warn("Simulation detected pre-existing balance being swept from a restricted
|
|
138
|
+
logger?.warn("Simulation detected pre-existing balance being swept from a restricted bundles contract", {
|
|
141
139
|
changes: drained.map((e) => ({
|
|
142
140
|
address: e.address,
|
|
143
141
|
token: e.token,
|
|
@@ -147,7 +145,7 @@ function assertNoBundlerRetention(params) {
|
|
|
147
145
|
}
|
|
148
146
|
const retained = entries.filter((e) => e.netChange > DUST_THRESHOLD);
|
|
149
147
|
if (retained.length > 0) {
|
|
150
|
-
throw new errors_js_1.BlacklistViolationError("Simulation detected asset transfers retained by restricted
|
|
148
|
+
throw new errors_js_1.BlacklistViolationError("Simulation detected asset transfers retained by restricted bundles contracts", retained.map((e) => ({
|
|
151
149
|
address: e.address,
|
|
152
150
|
token: e.token,
|
|
153
151
|
netRetained: e.netChange.toString(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { buildSimulationTxs } from "./build-simulation-txs.js";
|
|
2
|
-
export {
|
|
2
|
+
export { assertNoBundlesRetention } from "./bundles-retention.js";
|
|
3
3
|
export { executeSimulation } from "./execute-simulation.js";
|
|
4
4
|
export { validateInput } from "./validate-input.js";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateInput = exports.executeSimulation = exports.
|
|
3
|
+
exports.validateInput = exports.executeSimulation = exports.assertNoBundlesRetention = exports.buildSimulationTxs = void 0;
|
|
4
4
|
var build_simulation_txs_js_1 = require("./build-simulation-txs.js");
|
|
5
5
|
Object.defineProperty(exports, "buildSimulationTxs", { enumerable: true, get: function () { return build_simulation_txs_js_1.buildSimulationTxs; } });
|
|
6
|
-
var
|
|
7
|
-
Object.defineProperty(exports, "
|
|
6
|
+
var bundles_retention_js_1 = require("./bundles-retention.js");
|
|
7
|
+
Object.defineProperty(exports, "assertNoBundlesRetention", { enumerable: true, get: function () { return bundles_retention_js_1.assertNoBundlesRetention; } });
|
|
8
8
|
var execute_simulation_js_1 = require("./execute-simulation.js");
|
|
9
9
|
Object.defineProperty(exports, "executeSimulation", { enumerable: true, get: function () { return execute_simulation_js_1.executeSimulation; } });
|
|
10
10
|
var validate_input_js_1 = require("./validate-input.js");
|
|
@@ -7,7 +7,7 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
7
7
|
* budget → parses ERC20 transfers and WETH9 events from per-tx logs, restricting WETH9
|
|
8
8
|
* events to the registered wrapped-native token, rejecting them on known tokenless chains,
|
|
9
9
|
* and retaining signature-based parsing for unknown chains → asserts no funds are retained
|
|
10
|
-
* by
|
|
10
|
+
* by standalone `bundles` periphery contracts → returns the full result
|
|
11
11
|
* set. The caller reads whichever fields they need:
|
|
12
12
|
*
|
|
13
13
|
* - `transfers` → user-facing preview / server-side verification.
|
|
@@ -34,8 +34,8 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
34
34
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
35
35
|
* @throws {SimulationRevertedError} when the bundle reverts on either backend.
|
|
36
36
|
* @throws {BlacklistViolationError} when the simulation leaves value retained beyond
|
|
37
|
-
* the dust threshold by a
|
|
38
|
-
*
|
|
37
|
+
* the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
|
|
38
|
+
* VaultBundlesV1, BlueBundlesV1). Never bypassable.
|
|
39
39
|
* @throws {ExternalServiceError} (a) when both backends are unavailable within the
|
|
40
40
|
* timeout budget, or (b) when a backend returns a `calls` array whose length does
|
|
41
41
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
@@ -13,7 +13,7 @@ const index_js_2 = require("./pipeline/index.js");
|
|
|
13
13
|
* budget → parses ERC20 transfers and WETH9 events from per-tx logs, restricting WETH9
|
|
14
14
|
* events to the registered wrapped-native token, rejecting them on known tokenless chains,
|
|
15
15
|
* and retaining signature-based parsing for unknown chains → asserts no funds are retained
|
|
16
|
-
* by
|
|
16
|
+
* by standalone `bundles` periphery contracts → returns the full result
|
|
17
17
|
* set. The caller reads whichever fields they need:
|
|
18
18
|
*
|
|
19
19
|
* - `transfers` → user-facing preview / server-side verification.
|
|
@@ -40,8 +40,8 @@ const index_js_2 = require("./pipeline/index.js");
|
|
|
40
40
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
41
41
|
* @throws {SimulationRevertedError} when the bundle reverts on either backend.
|
|
42
42
|
* @throws {BlacklistViolationError} when the simulation leaves value retained beyond
|
|
43
|
-
* the dust threshold by a
|
|
44
|
-
*
|
|
43
|
+
* the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
|
|
44
|
+
* VaultBundlesV1, BlueBundlesV1). Never bypassable.
|
|
45
45
|
* @throws {ExternalServiceError} (a) when both backends are unavailable within the
|
|
46
46
|
* timeout budget, or (b) when a backend returns a `calls` array whose length does
|
|
47
47
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
@@ -88,7 +88,7 @@ async function simulate(config, params) {
|
|
|
88
88
|
wNative,
|
|
89
89
|
logger: config.logger,
|
|
90
90
|
});
|
|
91
|
-
(0, index_js_2.
|
|
91
|
+
(0, index_js_2.assertNoBundlesRetention)({
|
|
92
92
|
chainId: params.chainId,
|
|
93
93
|
transfers,
|
|
94
94
|
assetChanges: result.assetChanges,
|
package/lib/esm/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 restricted
|
|
22
|
+
/** Funds would flow to restricted standalone 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/esm/errors.js
CHANGED
|
@@ -19,7 +19,7 @@ export class SimulationRevertedError extends SimulationPackageError {
|
|
|
19
19
|
this.details = details;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
/** Funds would flow to restricted
|
|
22
|
+
/** Funds would flow to restricted standalone bundles contract addresses. Never bypassable. */
|
|
23
23
|
export class BlacklistViolationError extends SimulationPackageError {
|
|
24
24
|
assetChanges;
|
|
25
25
|
code = "BLACKLIST_ERROR";
|
|
@@ -13,7 +13,7 @@ export interface AssetChangeEntry {
|
|
|
13
13
|
* any real token address. `eth_simulateV1` synthesizes native moves from the
|
|
14
14
|
* sentinel `0xeee…eee`, and Tenderly may echo it — in checksummed or any other
|
|
15
15
|
* case — as an asset change's `contractAddress`. Native ETH is keyed by the exact
|
|
16
|
-
* `ethAddress` constant here and in the
|
|
16
|
+
* `ethAddress` constant here and in the bundles-retention guard, so a checksummed
|
|
17
17
|
* sentinel would land on a separate map key and silently escape retention checks.
|
|
18
18
|
* This is the single source of truth for token normalization shared by the
|
|
19
19
|
* transfer-log parser and the Tenderly asset-change mapper.
|
|
@@ -4,7 +4,7 @@ import { ethAddress, getAddress } from "viem";
|
|
|
4
4
|
* any real token address. `eth_simulateV1` synthesizes native moves from the
|
|
5
5
|
* sentinel `0xeee…eee`, and Tenderly may echo it — in checksummed or any other
|
|
6
6
|
* case — as an asset change's `contractAddress`. Native ETH is keyed by the exact
|
|
7
|
-
* `ethAddress` constant here and in the
|
|
7
|
+
* `ethAddress` constant here and in the bundles-retention guard, so a checksummed
|
|
8
8
|
* sentinel would land on a separate map key and silently escape retention checks.
|
|
9
9
|
* This is the single source of truth for token normalization shared by the
|
|
10
10
|
* transfer-log parser and the Tenderly asset-change mapper.
|
|
@@ -36,13 +36,17 @@ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d
|
|
|
36
36
|
* atomically inside a single call frame. A **non-canonical wrapped-native**
|
|
37
37
|
* that splits these emissions across two txs in the same bundle would leave
|
|
38
38
|
* a phantom zero-address `Transfer` in the parsed output, which can be summed
|
|
39
|
-
* by `
|
|
39
|
+
* by `assertNoBundlesRetention` and produce a false `BlacklistViolationError`.
|
|
40
40
|
* When its zero-address `Transfer` misses same-tx dedup, the parser emits a
|
|
41
41
|
* `warn` so the assumption break is observable before it reaches retention.
|
|
42
42
|
* None of `morpho-sdk`'s currently supported chains require cross-tx dedup;
|
|
43
43
|
* the assumption is rechecked when onboarding a new chain (see
|
|
44
44
|
* `evm-simulation/CLAUDE.md`).
|
|
45
45
|
*
|
|
46
|
+
* **Hex casing.** JSON-RPC `DATA` is case-insensitive, so `topics` and `data`
|
|
47
|
+
* are lowercased before signature dispatch and pair matching; a backend that
|
|
48
|
+
* echoes mixed-case hex cannot drop a transfer from the parsed output.
|
|
49
|
+
*
|
|
46
50
|
* **Failure mode:** on a per-log parse failure (malformed topic length,
|
|
47
51
|
* non-hex data), the log is skipped and a `warn` is emitted via the logger.
|
|
48
52
|
*
|
|
@@ -41,13 +41,17 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
41
41
|
* atomically inside a single call frame. A **non-canonical wrapped-native**
|
|
42
42
|
* that splits these emissions across two txs in the same bundle would leave
|
|
43
43
|
* a phantom zero-address `Transfer` in the parsed output, which can be summed
|
|
44
|
-
* by `
|
|
44
|
+
* by `assertNoBundlesRetention` and produce a false `BlacklistViolationError`.
|
|
45
45
|
* When its zero-address `Transfer` misses same-tx dedup, the parser emits a
|
|
46
46
|
* `warn` so the assumption break is observable before it reaches retention.
|
|
47
47
|
* None of `morpho-sdk`'s currently supported chains require cross-tx dedup;
|
|
48
48
|
* the assumption is rechecked when onboarding a new chain (see
|
|
49
49
|
* `evm-simulation/CLAUDE.md`).
|
|
50
50
|
*
|
|
51
|
+
* **Hex casing.** JSON-RPC `DATA` is case-insensitive, so `topics` and `data`
|
|
52
|
+
* are lowercased before signature dispatch and pair matching; a backend that
|
|
53
|
+
* echoes mixed-case hex cannot drop a transfer from the parsed output.
|
|
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
|
*
|
|
@@ -64,11 +68,17 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
64
68
|
export function parseTransfers(calls, options = {}) {
|
|
65
69
|
const transfers = [];
|
|
66
70
|
const { logger, wNative } = options;
|
|
67
|
-
|
|
71
|
+
// Lowercase `topics`/`data` so signature dispatch and pair matching are case-insensitive.
|
|
72
|
+
const normalizedCalls = calls.map((call) => call.logs.map((log) => ({
|
|
73
|
+
...log,
|
|
74
|
+
topics: log.topics.map((topic) => topic.toLowerCase()),
|
|
75
|
+
data: log.data.toLowerCase(),
|
|
76
|
+
})));
|
|
77
|
+
const wnativeShapedTokens = collectWnativeShapedTokens(normalizedCalls);
|
|
68
78
|
const acceptsWnativeEvent = (address) => wNative === undefined ||
|
|
69
79
|
(wNative !== null && isAddressEqual(address, wNative));
|
|
70
|
-
for (let txIdx = 0; txIdx <
|
|
71
|
-
const logs =
|
|
80
|
+
for (let txIdx = 0; txIdx < normalizedCalls.length; txIdx++) {
|
|
81
|
+
const logs = normalizedCalls[txIdx];
|
|
72
82
|
for (const log of logs) {
|
|
73
83
|
try {
|
|
74
84
|
const topic0 = log.topics[0];
|
|
@@ -191,8 +201,8 @@ function warnMalformed(logger, log, reason) {
|
|
|
191
201
|
/** Collect contracts that emit WETH9-shaped events anywhere in the bundle. */
|
|
192
202
|
function collectWnativeShapedTokens(calls) {
|
|
193
203
|
const tokens = new Set();
|
|
194
|
-
for (const
|
|
195
|
-
for (const log of
|
|
204
|
+
for (const logs of calls) {
|
|
205
|
+
for (const log of logs) {
|
|
196
206
|
const topic0 = log.topics[0];
|
|
197
207
|
if (topic0 === WITHDRAWAL_TOPIC || topic0 === DEPOSIT_TOPIC) {
|
|
198
208
|
tokens.add(log.address.toLowerCase());
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import type { AccountAssetChanges, SimulationLogger, Transfer } from "../../types.js";
|
|
2
|
-
interface
|
|
2
|
+
interface AssertNoBundlesRetentionParams {
|
|
3
3
|
chainId: number;
|
|
4
4
|
transfers: Transfer[];
|
|
5
5
|
assetChanges: readonly AccountAssetChanges[];
|
|
6
6
|
logger?: SimulationLogger;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
* Assert that no value is retained by
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
9
|
+
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
10
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
11
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
12
12
|
*
|
|
13
13
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
14
14
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
15
|
-
*
|
|
16
|
-
* positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
17
|
-
* address. Negative net flow means the simulated bundle sweeps a
|
|
18
|
-
* state-overridden balance. That is useful telemetry, but not
|
|
19
|
-
* retention, so it is warned instead of raising a blacklist
|
|
15
|
+
* bundles contract → vault), so gross inbound would fire false positives. We
|
|
16
|
+
* only block positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
17
|
+
* restricted address. Negative net flow means the simulated bundle sweeps a
|
|
18
|
+
* pre-existing or state-overridden balance. That is useful telemetry, but not
|
|
19
|
+
* current-bundle retention, so it is warned instead of raising a blacklist
|
|
20
|
+
* violation.
|
|
20
21
|
*
|
|
21
22
|
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
22
23
|
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
@@ -25,19 +26,19 @@ interface AssertNoBundlerRetentionParams {
|
|
|
25
26
|
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
26
27
|
* truth for native value: Tenderly derives it from its trace, and
|
|
27
28
|
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
28
|
-
* this, native ETH stuck in
|
|
29
|
-
* Tenderly path (Cantina finding 1440).
|
|
29
|
+
* this, native ETH stuck in a bundles contract would pass the guard undetected
|
|
30
|
+
* on the Tenderly path (Cantina finding 1440).
|
|
30
31
|
*
|
|
31
32
|
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
32
33
|
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
33
34
|
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
34
|
-
* fallback for
|
|
35
|
+
* fallback for bundles addresses that carry no native `assetChanges` entry.
|
|
35
36
|
*
|
|
36
37
|
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
37
38
|
* package's public API (only re-exported from the internal pipeline barrel).
|
|
38
|
-
* @param params.chainId - Chain whose blue-sdk `
|
|
39
|
-
*
|
|
40
|
-
* skipped (a `logger.warn` records the skip).
|
|
39
|
+
* @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundles`
|
|
40
|
+
* registry entries define the restricted address set. Chains cataloging
|
|
41
|
+
* neither are skipped (a `logger.warn` records the skip).
|
|
41
42
|
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
42
43
|
* `eth_simulateV1` synthetic native sentinel) scanned for net retention.
|
|
43
44
|
* @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
|
|
@@ -47,7 +48,8 @@ interface AssertNoBundlerRetentionParams {
|
|
|
47
48
|
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
48
49
|
* above `DUST_THRESHOLD`; otherwise throws.
|
|
49
50
|
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
50
|
-
* `
|
|
51
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
52
|
+
* token.
|
|
51
53
|
*/
|
|
52
|
-
export declare function
|
|
54
|
+
export declare function assertNoBundlesRetention(params: AssertNoBundlesRetentionParams): void;
|
|
53
55
|
export {};
|
|
@@ -3,27 +3,28 @@ import { isDefined } from "@morpho-org/morpho-ts";
|
|
|
3
3
|
import { ethAddress, getAddress } from "viem";
|
|
4
4
|
import { BlacklistViolationError } from "../../errors.js";
|
|
5
5
|
/**
|
|
6
|
-
* Dust tolerance for
|
|
6
|
+
* Dust tolerance for standalone bundles retention, in raw token units.
|
|
7
7
|
*
|
|
8
8
|
* 100 units is negligible for 18-decimal tokens (~1e-16). For low-decimal
|
|
9
9
|
* tokens (e.g., 2-decimal stablecoins) this represents ~1.00 token — worth
|
|
10
10
|
* keeping in mind. The threshold is intentionally token-agnostic: its purpose
|
|
11
|
-
* is to absorb rounding noise
|
|
12
|
-
*
|
|
11
|
+
* is to absorb routing rounding noise, not to define an acceptable retention
|
|
12
|
+
* amount.
|
|
13
13
|
*/
|
|
14
14
|
const DUST_THRESHOLD = 100n;
|
|
15
15
|
/**
|
|
16
|
-
* Assert that no value is retained by
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
16
|
+
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
17
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
18
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
19
19
|
*
|
|
20
20
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
21
21
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
22
|
-
*
|
|
23
|
-
* positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
24
|
-
* address. Negative net flow means the simulated bundle sweeps a
|
|
25
|
-
* state-overridden balance. That is useful telemetry, but not
|
|
26
|
-
* retention, so it is warned instead of raising a blacklist
|
|
22
|
+
* bundles contract → vault), so gross inbound would fire false positives. We
|
|
23
|
+
* only block positive net flow above `DUST_THRESHOLD` — value stuck in a
|
|
24
|
+
* restricted address. Negative net flow means the simulated bundle sweeps a
|
|
25
|
+
* pre-existing or state-overridden balance. That is useful telemetry, but not
|
|
26
|
+
* current-bundle retention, so it is warned instead of raising a blacklist
|
|
27
|
+
* violation.
|
|
27
28
|
*
|
|
28
29
|
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
29
30
|
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
@@ -32,19 +33,19 @@ const DUST_THRESHOLD = 100n;
|
|
|
32
33
|
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
33
34
|
* truth for native value: Tenderly derives it from its trace, and
|
|
34
35
|
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
35
|
-
* this, native ETH stuck in
|
|
36
|
-
* Tenderly path (Cantina finding 1440).
|
|
36
|
+
* this, native ETH stuck in a bundles contract would pass the guard undetected
|
|
37
|
+
* on the Tenderly path (Cantina finding 1440).
|
|
37
38
|
*
|
|
38
39
|
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
39
40
|
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
40
41
|
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
41
|
-
* fallback for
|
|
42
|
+
* fallback for bundles addresses that carry no native `assetChanges` entry.
|
|
42
43
|
*
|
|
43
44
|
* @internal Internal pipeline stage, composed by `simulate`; not part of the
|
|
44
45
|
* package's public API (only re-exported from the internal pipeline barrel).
|
|
45
|
-
* @param params.chainId - Chain whose blue-sdk `
|
|
46
|
-
*
|
|
47
|
-
* skipped (a `logger.warn` records the skip).
|
|
46
|
+
* @param params.chainId - Chain whose blue-sdk `bundles` and `midnightBundles`
|
|
47
|
+
* registry entries define the restricted address set. Chains cataloging
|
|
48
|
+
* neither are skipped (a `logger.warn` records the skip).
|
|
48
49
|
* @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
|
|
49
50
|
* `eth_simulateV1` synthetic native sentinel) scanned for net retention.
|
|
50
51
|
* @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
|
|
@@ -54,15 +55,13 @@ const DUST_THRESHOLD = 100n;
|
|
|
54
55
|
* @returns Nothing. Returns silently when no restricted contract retains value
|
|
55
56
|
* above `DUST_THRESHOLD`; otherwise throws.
|
|
56
57
|
* @throws {BlacklistViolationError} when net inbound flow to a restricted
|
|
57
|
-
* `
|
|
58
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
59
|
+
* token.
|
|
58
60
|
*/
|
|
59
|
-
export function
|
|
61
|
+
export function assertNoBundlesRetention(params) {
|
|
60
62
|
const { chainId, transfers, assetChanges, logger } = params;
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`). Both families are
|
|
64
|
-
// transient intermediaries that route user value and must never retain it, so
|
|
65
|
-
// both are guarded identically.
|
|
63
|
+
// Standalone bundles contracts and `MidnightBundlesV1` are transient
|
|
64
|
+
// intermediaries that route user value and must never retain it.
|
|
66
65
|
let addresses;
|
|
67
66
|
try {
|
|
68
67
|
addresses = getChainAddresses(chainId);
|
|
@@ -79,18 +78,17 @@ export function assertNoBundlerRetention(params) {
|
|
|
79
78
|
throw error;
|
|
80
79
|
}
|
|
81
80
|
const restrictedAddresses = new Set();
|
|
82
|
-
if (addresses.bundler3) {
|
|
83
|
-
for (const addr of Object.values(addresses.bundler3).filter(isDefined))
|
|
84
|
-
restrictedAddresses.add(getAddress(addr));
|
|
85
|
-
}
|
|
86
81
|
if (addresses.bundles) {
|
|
87
82
|
for (const addr of Object.values(addresses.bundles).filter(isDefined))
|
|
88
83
|
restrictedAddresses.add(getAddress(addr));
|
|
89
84
|
}
|
|
85
|
+
if (addresses.midnightBundles) {
|
|
86
|
+
restrictedAddresses.add(getAddress(addresses.midnightBundles));
|
|
87
|
+
}
|
|
90
88
|
if (restrictedAddresses.size === 0) {
|
|
91
|
-
// blue-sdk knows the chain but cataloged
|
|
92
|
-
//
|
|
93
|
-
logger?.warn("Chain known to blue-sdk but has no
|
|
89
|
+
// blue-sdk knows the chain but cataloged no restricted intermediary for it.
|
|
90
|
+
// Treat the same as UnsupportedChainIdError — retention check skipped.
|
|
91
|
+
logger?.warn("Chain known to blue-sdk but has no restricted intermediary config, retention check skipped", { chainId });
|
|
94
92
|
return;
|
|
95
93
|
}
|
|
96
94
|
// Map keyed by (address, token) → structured entry. Avoids string parse-back.
|
|
@@ -107,8 +105,8 @@ export function assertNoBundlerRetention(params) {
|
|
|
107
105
|
}
|
|
108
106
|
};
|
|
109
107
|
// Native ETH from `assetChanges` (authoritative, cross-backend). Track which
|
|
110
|
-
//
|
|
111
|
-
// does not re-add the same native move on `eth_simulateV1`.
|
|
108
|
+
// restricted contracts already have a native entry here so the transfer-log
|
|
109
|
+
// pass below does not re-add the same native move on `eth_simulateV1`.
|
|
112
110
|
const nativeFromAssetChanges = new Set();
|
|
113
111
|
for (const { account, changes } of assetChanges) {
|
|
114
112
|
if (!restrictedAddresses.has(account))
|
|
@@ -121,9 +119,9 @@ export function assertNoBundlerRetention(params) {
|
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
// ERC20 / WETH9 retention from transfer logs. Native ETH transfers (the
|
|
124
|
-
// `eth_simulateV1` synthetic sentinel) only backfill a
|
|
125
|
-
// is absent from `nativeFromAssetChanges`, so native is never summed on
|
|
126
|
-
// of the `assetChanges` value already recorded for that address.
|
|
122
|
+
// `eth_simulateV1` synthetic sentinel) only backfill a restricted endpoint
|
|
123
|
+
// that is absent from `nativeFromAssetChanges`, so native is never summed on
|
|
124
|
+
// top of the `assetChanges` value already recorded for that address.
|
|
127
125
|
const usesAssetChangeNative = (t, addr) => t.token === ethAddress && nativeFromAssetChanges.has(addr.toLowerCase());
|
|
128
126
|
for (const t of transfers) {
|
|
129
127
|
if (restrictedAddresses.has(t.to) && !usesAssetChangeNative(t, t.to))
|
|
@@ -134,7 +132,7 @@ export function assertNoBundlerRetention(params) {
|
|
|
134
132
|
const entries = [...flow.values()];
|
|
135
133
|
const drained = entries.filter((e) => e.netChange < -DUST_THRESHOLD);
|
|
136
134
|
if (drained.length > 0) {
|
|
137
|
-
logger?.warn("Simulation detected pre-existing balance being swept from a restricted
|
|
135
|
+
logger?.warn("Simulation detected pre-existing balance being swept from a restricted bundles contract", {
|
|
138
136
|
changes: drained.map((e) => ({
|
|
139
137
|
address: e.address,
|
|
140
138
|
token: e.token,
|
|
@@ -144,7 +142,7 @@ export function assertNoBundlerRetention(params) {
|
|
|
144
142
|
}
|
|
145
143
|
const retained = entries.filter((e) => e.netChange > DUST_THRESHOLD);
|
|
146
144
|
if (retained.length > 0) {
|
|
147
|
-
throw new BlacklistViolationError("Simulation detected asset transfers retained by restricted
|
|
145
|
+
throw new BlacklistViolationError("Simulation detected asset transfers retained by restricted bundles contracts", retained.map((e) => ({
|
|
148
146
|
address: e.address,
|
|
149
147
|
token: e.token,
|
|
150
148
|
netRetained: e.netChange.toString(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { buildSimulationTxs } from "./build-simulation-txs.js";
|
|
2
|
-
export {
|
|
2
|
+
export { assertNoBundlesRetention } from "./bundles-retention.js";
|
|
3
3
|
export { executeSimulation } from "./execute-simulation.js";
|
|
4
4
|
export { validateInput } from "./validate-input.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { buildSimulationTxs } from "./build-simulation-txs.js";
|
|
2
|
-
export {
|
|
2
|
+
export { assertNoBundlesRetention } from "./bundles-retention.js";
|
|
3
3
|
export { executeSimulation } from "./execute-simulation.js";
|
|
4
4
|
export { validateInput } from "./validate-input.js";
|
|
@@ -7,7 +7,7 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
7
7
|
* budget → parses ERC20 transfers and WETH9 events from per-tx logs, restricting WETH9
|
|
8
8
|
* events to the registered wrapped-native token, rejecting them on known tokenless chains,
|
|
9
9
|
* and retaining signature-based parsing for unknown chains → asserts no funds are retained
|
|
10
|
-
* by
|
|
10
|
+
* by standalone `bundles` periphery contracts → returns the full result
|
|
11
11
|
* set. The caller reads whichever fields they need:
|
|
12
12
|
*
|
|
13
13
|
* - `transfers` → user-facing preview / server-side verification.
|
|
@@ -34,8 +34,8 @@ import type { SimulateParams, SimulationConfig, SimulationResult } from "../type
|
|
|
34
34
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
35
35
|
* @throws {SimulationRevertedError} when the bundle reverts on either backend.
|
|
36
36
|
* @throws {BlacklistViolationError} when the simulation leaves value retained beyond
|
|
37
|
-
* the dust threshold by a
|
|
38
|
-
*
|
|
37
|
+
* the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
|
|
38
|
+
* VaultBundlesV1, BlueBundlesV1). Never bypassable.
|
|
39
39
|
* @throws {ExternalServiceError} (a) when both backends are unavailable within the
|
|
40
40
|
* timeout budget, or (b) when a backend returns a `calls` array whose length does
|
|
41
41
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _try, getChainAddresses, UnsupportedChainIdError, } from "@morpho-org/blue-sdk";
|
|
2
2
|
import { ExternalServiceError } from "../errors.js";
|
|
3
3
|
import { parseTransfers } from "./parsing/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { assertNoBundlesRetention, buildSimulationTxs, executeSimulation, validateInput, } from "./pipeline/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* Simulate a bundle of EVM transactions.
|
|
7
7
|
*
|
|
@@ -10,7 +10,7 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
|
|
|
10
10
|
* budget → parses ERC20 transfers and WETH9 events from per-tx logs, restricting WETH9
|
|
11
11
|
* events to the registered wrapped-native token, rejecting them on known tokenless chains,
|
|
12
12
|
* and retaining signature-based parsing for unknown chains → asserts no funds are retained
|
|
13
|
-
* by
|
|
13
|
+
* by standalone `bundles` periphery contracts → returns the full result
|
|
14
14
|
* set. The caller reads whichever fields they need:
|
|
15
15
|
*
|
|
16
16
|
* - `transfers` → user-facing preview / server-side verification.
|
|
@@ -37,8 +37,8 @@ import { assertNoBundlerRetention, buildSimulationTxs, executeSimulation, valida
|
|
|
37
37
|
* @throws {UnsupportedChainError} when the chain is not configured for any backend.
|
|
38
38
|
* @throws {SimulationRevertedError} when the bundle reverts on either backend.
|
|
39
39
|
* @throws {BlacklistViolationError} when the simulation leaves value retained beyond
|
|
40
|
-
* the dust threshold by a
|
|
41
|
-
*
|
|
40
|
+
* the dust threshold by a `bundles` periphery contract (VaultExitBundlesV1,
|
|
41
|
+
* VaultBundlesV1, BlueBundlesV1). Never bypassable.
|
|
42
42
|
* @throws {ExternalServiceError} (a) when both backends are unavailable within the
|
|
43
43
|
* timeout budget, or (b) when a backend returns a `calls` array whose length does
|
|
44
44
|
* not match the resolved `simulationTxs` — refusing to map transfers with mismatched
|
|
@@ -85,7 +85,7 @@ export async function simulate(config, params) {
|
|
|
85
85
|
wNative,
|
|
86
86
|
logger: config.logger,
|
|
87
87
|
});
|
|
88
|
-
|
|
88
|
+
assertNoBundlesRetention({
|
|
89
89
|
chainId: params.chainId,
|
|
90
90
|
transfers,
|
|
91
91
|
assetChanges: result.assetChanges,
|
package/package.json
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morpho-org/evm-simulation",
|
|
3
3
|
"description": "EVM simulation engine for Morpho transactions.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.2.0-next.2",
|
|
5
5
|
"author": "Morpho Association <contact@morpho.org>",
|
|
6
|
-
"contributors": [
|
|
7
|
-
"Rubilmax <rmilon@gmail.com>"
|
|
8
|
-
],
|
|
9
6
|
"repository": {
|
|
10
7
|
"type": "git",
|
|
11
8
|
"url": "git+https://github.com/morpho-org/sdks.git",
|
|
@@ -25,8 +22,8 @@
|
|
|
25
22
|
],
|
|
26
23
|
"dependencies": {
|
|
27
24
|
"zod": "^4.4.3",
|
|
28
|
-
"@morpho-org/blue-sdk": "^
|
|
29
|
-
"@morpho-org/morpho-ts": "^
|
|
25
|
+
"@morpho-org/blue-sdk": "^7.0.0-next.2",
|
|
26
|
+
"@morpho-org/morpho-ts": "^3.0.0-next.1"
|
|
30
27
|
},
|
|
31
28
|
"peerDependencies": {
|
|
32
29
|
"viem": "^2.0.0"
|