@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.
@@ -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
- const wnativeShapedTokens = collectWnativeShapedTokens(calls);
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 < calls.length; txIdx++) {
75
- const logs = calls[txIdx].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 call of calls) {
199
- for (const log of call.logs) {
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` registry defines the
39
- * restricted address set. Chains without one are skipped (a `logger.warn`
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 token.
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` registry defines the
49
- * restricted address set. Chains without one are skipped (a `logger.warn`
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 token.
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 intermediaries that route user
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 bundles contracts. Treat the
88
- // same as UnsupportedChainIdError — retention check skipped.
89
- logger?.warn("Chain known to blue-sdk but has no bundles config, retention check skipped", { chainId });
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
- const wnativeShapedTokens = collectWnativeShapedTokens(calls);
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 < calls.length; txIdx++) {
71
- const logs = calls[txIdx].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 call of calls) {
195
- for (const log of call.logs) {
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` registry defines the
39
- * restricted address set. Chains without one are skipped (a `logger.warn`
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 token.
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` registry defines the
46
- * restricted address set. Chains without one are skipped (a `logger.warn`
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 token.
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 intermediaries that route user
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 bundles contracts. Treat the
85
- // same as UnsupportedChainIdError — retention check skipped.
86
- logger?.warn("Chain known to blue-sdk but has no bundles config, retention check skipped", { chainId });
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-next.1",
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-next.1",
29
- "@morpho-org/morpho-ts": "^3.0.0-next.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.10",
34
+ "vitest": "^4.1.11",
38
35
  "@morpho-org/test": "^2.10.0"
39
36
  },
40
37
  "scripts": {