@morpho-org/evm-simulation 4.2.0-next.1 → 4.2.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/simulate/parsing/transfers.d.ts +4 -0
- package/lib/cjs/simulate/parsing/transfers.js +15 -5
- package/lib/cjs/simulate/pipeline/bundles-retention.d.ts +7 -5
- package/lib/cjs/simulate/pipeline/bundles-retention.js +15 -10
- package/lib/esm/simulate/parsing/transfers.d.ts +4 -0
- package/lib/esm/simulate/parsing/transfers.js +15 -5
- package/lib/esm/simulate/pipeline/bundles-retention.d.ts +7 -5
- package/lib/esm/simulate/pipeline/bundles-retention.js +15 -10
- package/package.json +4 -7
|
@@ -43,6 +43,10 @@ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d
|
|
|
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
|
*
|
|
@@ -52,6 +52,10 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
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());
|
|
@@ -7,7 +7,8 @@ interface AssertNoBundlesRetentionParams {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
10
|
-
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`)
|
|
10
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
11
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
11
12
|
*
|
|
12
13
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
13
14
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
@@ -35,9 +36,9 @@ interface AssertNoBundlesRetentionParams {
|
|
|
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 `bundles`
|
|
39
|
-
* restricted address set. Chains
|
|
40
|
-
* 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 AssertNoBundlesRetentionParams {
|
|
|
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
|
-
* `bundles` contract exceeds `DUST_THRESHOLD` for any
|
|
51
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
52
|
+
* token.
|
|
51
53
|
*/
|
|
52
54
|
export declare function assertNoBundlesRetention(params: AssertNoBundlesRetentionParams): void;
|
|
53
55
|
export {};
|
|
@@ -17,7 +17,8 @@ const errors_js_1 = require("../../errors.js");
|
|
|
17
17
|
const DUST_THRESHOLD = 100n;
|
|
18
18
|
/**
|
|
19
19
|
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
20
|
-
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`)
|
|
20
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
21
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
21
22
|
*
|
|
22
23
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
23
24
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
@@ -45,9 +46,9 @@ const DUST_THRESHOLD = 100n;
|
|
|
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 `bundles`
|
|
49
|
-
* restricted address set. Chains
|
|
50
|
-
* 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,12 +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
|
-
* `bundles` contract exceeds `DUST_THRESHOLD` for any
|
|
61
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
62
|
+
* token.
|
|
61
63
|
*/
|
|
62
64
|
function assertNoBundlesRetention(params) {
|
|
63
65
|
const { chainId, transfers, assetChanges, logger } = params;
|
|
64
|
-
// Standalone bundles contracts are transient
|
|
65
|
-
// value and must never retain it.
|
|
66
|
+
// Standalone bundles contracts and `MidnightBundlesV1` are transient
|
|
67
|
+
// intermediaries that route user value and must never retain it.
|
|
66
68
|
let addresses;
|
|
67
69
|
try {
|
|
68
70
|
addresses = (0, blue_sdk_1.getChainAddresses)(chainId);
|
|
@@ -83,10 +85,13 @@ function assertNoBundlesRetention(params) {
|
|
|
83
85
|
for (const addr of Object.values(addresses.bundles).filter(morpho_ts_1.isDefined))
|
|
84
86
|
restrictedAddresses.add((0, viem_1.getAddress)(addr));
|
|
85
87
|
}
|
|
88
|
+
if (addresses.midnightBundles) {
|
|
89
|
+
restrictedAddresses.add((0, viem_1.getAddress)(addresses.midnightBundles));
|
|
90
|
+
}
|
|
86
91
|
if (restrictedAddresses.size === 0) {
|
|
87
|
-
// blue-sdk knows the chain but cataloged no
|
|
88
|
-
// same as UnsupportedChainIdError — retention check skipped.
|
|
89
|
-
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 });
|
|
90
95
|
return;
|
|
91
96
|
}
|
|
92
97
|
// Map keyed by (address, token) → structured entry. Avoids string parse-back.
|
|
@@ -43,6 +43,10 @@ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d
|
|
|
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
|
*
|
|
@@ -48,6 +48,10 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
|
|
|
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());
|
|
@@ -7,7 +7,8 @@ interface AssertNoBundlesRetentionParams {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
10
|
-
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`)
|
|
10
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
11
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
11
12
|
*
|
|
12
13
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
13
14
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
@@ -35,9 +36,9 @@ interface AssertNoBundlesRetentionParams {
|
|
|
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 `bundles`
|
|
39
|
-
* restricted address set. Chains
|
|
40
|
-
* 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 AssertNoBundlesRetentionParams {
|
|
|
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
|
-
* `bundles` contract exceeds `DUST_THRESHOLD` for any
|
|
51
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
52
|
+
* token.
|
|
51
53
|
*/
|
|
52
54
|
export declare function assertNoBundlesRetention(params: AssertNoBundlesRetentionParams): void;
|
|
53
55
|
export {};
|
|
@@ -14,7 +14,8 @@ import { BlacklistViolationError } from "../../errors.js";
|
|
|
14
14
|
const DUST_THRESHOLD = 100n;
|
|
15
15
|
/**
|
|
16
16
|
* Assert that no value is retained by standalone `bundles` periphery contracts
|
|
17
|
-
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`)
|
|
17
|
+
* (`VaultExitBundlesV1`, `VaultBundlesV1`, `BlueBundlesV1`) or by
|
|
18
|
+
* `MidnightBundlesV1` (`midnightBundles`).
|
|
18
19
|
*
|
|
19
20
|
* Uses net flow (inbound minus outbound) per (restricted address, token) pair.
|
|
20
21
|
* These contracts legitimately receive tokens as an intermediary (user →
|
|
@@ -42,9 +43,9 @@ const DUST_THRESHOLD = 100n;
|
|
|
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 `bundles`
|
|
46
|
-
* restricted address set. Chains
|
|
47
|
-
* 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,12 +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
|
-
* `bundles` contract exceeds `DUST_THRESHOLD` for any
|
|
58
|
+
* `bundles` or `midnightBundles` contract exceeds `DUST_THRESHOLD` for any
|
|
59
|
+
* token.
|
|
58
60
|
*/
|
|
59
61
|
export function assertNoBundlesRetention(params) {
|
|
60
62
|
const { chainId, transfers, assetChanges, logger } = params;
|
|
61
|
-
// Standalone bundles contracts are transient
|
|
62
|
-
// value and must never retain it.
|
|
63
|
+
// Standalone bundles contracts and `MidnightBundlesV1` are transient
|
|
64
|
+
// intermediaries that route user value and must never retain it.
|
|
63
65
|
let addresses;
|
|
64
66
|
try {
|
|
65
67
|
addresses = getChainAddresses(chainId);
|
|
@@ -80,10 +82,13 @@ export function assertNoBundlesRetention(params) {
|
|
|
80
82
|
for (const addr of Object.values(addresses.bundles).filter(isDefined))
|
|
81
83
|
restrictedAddresses.add(getAddress(addr));
|
|
82
84
|
}
|
|
85
|
+
if (addresses.midnightBundles) {
|
|
86
|
+
restrictedAddresses.add(getAddress(addresses.midnightBundles));
|
|
87
|
+
}
|
|
83
88
|
if (restrictedAddresses.size === 0) {
|
|
84
|
-
// blue-sdk knows the chain but cataloged no
|
|
85
|
-
// same as UnsupportedChainIdError — retention check skipped.
|
|
86
|
-
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 });
|
|
87
92
|
return;
|
|
88
93
|
}
|
|
89
94
|
// Map keyed by (address, token) → structured entry. Avoids string parse-back.
|
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.2.0
|
|
4
|
+
"version": "4.2.0",
|
|
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": "^7.0.0
|
|
29
|
-
"@morpho-org/morpho-ts": "^3.0.0
|
|
25
|
+
"@morpho-org/blue-sdk": "^7.0.0",
|
|
26
|
+
"@morpho-org/morpho-ts": "^3.0.0"
|
|
30
27
|
},
|
|
31
28
|
"peerDependencies": {
|
|
32
29
|
"viem": "^2.0.0"
|
|
@@ -34,7 +31,7 @@
|
|
|
34
31
|
"devDependencies": {
|
|
35
32
|
"typescript": "7.0.2",
|
|
36
33
|
"viem": "^2.55.10",
|
|
37
|
-
"vitest": "^4.1.
|
|
34
|
+
"vitest": "^4.1.11",
|
|
38
35
|
"@morpho-org/test": "^2.10.0"
|
|
39
36
|
},
|
|
40
37
|
"scripts": {
|