@morpho-org/evm-simulation 4.1.0 → 4.1.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/simulate/pipeline/bundler-retention.d.ts +18 -2
- package/lib/cjs/simulate/pipeline/bundler-retention.js +38 -4
- package/lib/cjs/simulate/simulate.js +1 -0
- package/lib/esm/simulate/pipeline/bundler-retention.d.ts +18 -2
- package/lib/esm/simulate/pipeline/bundler-retention.js +39 -5
- package/lib/esm/simulate/simulate.js +1 -0
- package/package.json +6 -5
- package/src/index.ts +0 -28
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { SimulationLogger, Transfer } from "../../types.js";
|
|
1
|
+
import type { AccountAssetChanges, SimulationLogger, Transfer } from "../../types.js";
|
|
2
2
|
interface AssertNoBundlerRetentionParams {
|
|
3
3
|
chainId: number;
|
|
4
4
|
transfers: Transfer[];
|
|
5
|
+
assetChanges: readonly AccountAssetChanges[];
|
|
5
6
|
logger?: SimulationLogger;
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
@@ -10,10 +11,25 @@ interface AssertNoBundlerRetentionParams {
|
|
|
10
11
|
* Uses net flow (inbound minus outbound) per (bundler address, token) pair.
|
|
11
12
|
* Bundler3 legitimately receives tokens as an intermediary (user → bundler3 →
|
|
12
13
|
* vault), so gross inbound would fire false positives. We only block positive
|
|
13
|
-
* net flow above `DUST_THRESHOLD` —
|
|
14
|
+
* net flow above `DUST_THRESHOLD` — value stuck in a bundler address. Negative
|
|
14
15
|
* net flow means the simulated bundle sweeps a pre-existing or state-overridden
|
|
15
16
|
* bundler balance. That is useful telemetry, but not current-bundle retention,
|
|
16
17
|
* so it is warned instead of raising a blacklist violation.
|
|
18
|
+
*
|
|
19
|
+
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
20
|
+
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
21
|
+
* emits no event log, so it can never appear in `transfers` on the Tenderly
|
|
22
|
+
* primary backend — Tenderly derives native moves into `assetChanges` instead.
|
|
23
|
+
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
24
|
+
* truth for native value: Tenderly derives it from its trace, and
|
|
25
|
+
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
26
|
+
* this, native ETH stuck in bundler3 would pass the guard undetected on the
|
|
27
|
+
* Tenderly path (Cantina finding 1440).
|
|
28
|
+
*
|
|
29
|
+
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
30
|
+
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
31
|
+
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
32
|
+
* fallback for bundler addresses that carry no native `assetChanges` entry.
|
|
17
33
|
*/
|
|
18
34
|
export declare function assertNoBundlerRetention(params: AssertNoBundlerRetentionParams): void;
|
|
19
35
|
export {};
|
|
@@ -48,13 +48,28 @@ function getBundlerAddresses(chainId, logger) {
|
|
|
48
48
|
* Uses net flow (inbound minus outbound) per (bundler address, token) pair.
|
|
49
49
|
* Bundler3 legitimately receives tokens as an intermediary (user → bundler3 →
|
|
50
50
|
* vault), so gross inbound would fire false positives. We only block positive
|
|
51
|
-
* net flow above `DUST_THRESHOLD` —
|
|
51
|
+
* net flow above `DUST_THRESHOLD` — value stuck in a bundler address. Negative
|
|
52
52
|
* net flow means the simulated bundle sweeps a pre-existing or state-overridden
|
|
53
53
|
* bundler balance. That is useful telemetry, but not current-bundle retention,
|
|
54
54
|
* so it is warned instead of raising a blacklist violation.
|
|
55
|
+
*
|
|
56
|
+
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
57
|
+
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
58
|
+
* emits no event log, so it can never appear in `transfers` on the Tenderly
|
|
59
|
+
* primary backend — Tenderly derives native moves into `assetChanges` instead.
|
|
60
|
+
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
61
|
+
* truth for native value: Tenderly derives it from its trace, and
|
|
62
|
+
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
63
|
+
* this, native ETH stuck in bundler3 would pass the guard undetected on the
|
|
64
|
+
* Tenderly path (Cantina finding 1440).
|
|
65
|
+
*
|
|
66
|
+
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
67
|
+
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
68
|
+
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
69
|
+
* fallback for bundler addresses that carry no native `assetChanges` entry.
|
|
55
70
|
*/
|
|
56
71
|
function assertNoBundlerRetention(params) {
|
|
57
|
-
const { chainId, transfers, logger } = params;
|
|
72
|
+
const { chainId, transfers, assetChanges, logger } = params;
|
|
58
73
|
const bundlerAddresses = getBundlerAddresses(chainId, logger);
|
|
59
74
|
if (bundlerAddresses.size === 0)
|
|
60
75
|
return;
|
|
@@ -71,10 +86,29 @@ function assertNoBundlerRetention(params) {
|
|
|
71
86
|
flow.set(key, { address, token, netChange: delta });
|
|
72
87
|
}
|
|
73
88
|
};
|
|
89
|
+
// Native ETH from `assetChanges` (authoritative, cross-backend). Track which
|
|
90
|
+
// bundlers already have a native entry here so the transfer-log pass below
|
|
91
|
+
// does not re-add the same native move on `eth_simulateV1`.
|
|
92
|
+
const nativeFromAssetChanges = new Set();
|
|
93
|
+
for (const { account, changes } of assetChanges) {
|
|
94
|
+
if (!bundlerAddresses.has(account))
|
|
95
|
+
continue;
|
|
96
|
+
for (const change of changes) {
|
|
97
|
+
if (change.token !== viem_1.ethAddress)
|
|
98
|
+
continue;
|
|
99
|
+
recordFlow(account, viem_1.ethAddress, change.diff);
|
|
100
|
+
nativeFromAssetChanges.add(account.toLowerCase());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ERC20 / WETH9 retention from transfer logs. Native ETH transfers (the
|
|
104
|
+
// `eth_simulateV1` synthetic sentinel) only backfill a bundler endpoint that
|
|
105
|
+
// is absent from `nativeFromAssetChanges`, so native is never summed on top
|
|
106
|
+
// of the `assetChanges` value already recorded for that address.
|
|
107
|
+
const usesAssetChangeNative = (t, addr) => t.token === viem_1.ethAddress && nativeFromAssetChanges.has(addr.toLowerCase());
|
|
74
108
|
for (const t of transfers) {
|
|
75
|
-
if (bundlerAddresses.has(t.to))
|
|
109
|
+
if (bundlerAddresses.has(t.to) && !usesAssetChangeNative(t, t.to))
|
|
76
110
|
recordFlow(t.to, t.token, t.amount);
|
|
77
|
-
if (bundlerAddresses.has(t.from))
|
|
111
|
+
if (bundlerAddresses.has(t.from) && !usesAssetChangeNative(t, t.from))
|
|
78
112
|
recordFlow(t.from, t.token, -t.amount);
|
|
79
113
|
}
|
|
80
114
|
const entries = [...flow.values()];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { SimulationLogger, Transfer } from "../../types.js";
|
|
1
|
+
import type { AccountAssetChanges, SimulationLogger, Transfer } from "../../types.js";
|
|
2
2
|
interface AssertNoBundlerRetentionParams {
|
|
3
3
|
chainId: number;
|
|
4
4
|
transfers: Transfer[];
|
|
5
|
+
assetChanges: readonly AccountAssetChanges[];
|
|
5
6
|
logger?: SimulationLogger;
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
@@ -10,10 +11,25 @@ interface AssertNoBundlerRetentionParams {
|
|
|
10
11
|
* Uses net flow (inbound minus outbound) per (bundler address, token) pair.
|
|
11
12
|
* Bundler3 legitimately receives tokens as an intermediary (user → bundler3 →
|
|
12
13
|
* vault), so gross inbound would fire false positives. We only block positive
|
|
13
|
-
* net flow above `DUST_THRESHOLD` —
|
|
14
|
+
* net flow above `DUST_THRESHOLD` — value stuck in a bundler address. Negative
|
|
14
15
|
* net flow means the simulated bundle sweeps a pre-existing or state-overridden
|
|
15
16
|
* bundler balance. That is useful telemetry, but not current-bundle retention,
|
|
16
17
|
* so it is warned instead of raising a blacklist violation.
|
|
18
|
+
*
|
|
19
|
+
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
20
|
+
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
21
|
+
* emits no event log, so it can never appear in `transfers` on the Tenderly
|
|
22
|
+
* primary backend — Tenderly derives native moves into `assetChanges` instead.
|
|
23
|
+
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
24
|
+
* truth for native value: Tenderly derives it from its trace, and
|
|
25
|
+
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
26
|
+
* this, native ETH stuck in bundler3 would pass the guard undetected on the
|
|
27
|
+
* Tenderly path (Cantina finding 1440).
|
|
28
|
+
*
|
|
29
|
+
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
30
|
+
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
31
|
+
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
32
|
+
* fallback for bundler addresses that carry no native `assetChanges` entry.
|
|
17
33
|
*/
|
|
18
34
|
export declare function assertNoBundlerRetention(params: AssertNoBundlerRetentionParams): void;
|
|
19
35
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getChainAddresses, UnsupportedChainIdError, } from "@morpho-org/blue-sdk";
|
|
2
2
|
import { isDefined } from "@morpho-org/morpho-ts";
|
|
3
|
-
import { getAddress } from "viem";
|
|
3
|
+
import { ethAddress, getAddress } from "viem";
|
|
4
4
|
import { BlacklistViolationError } from "../../errors.js";
|
|
5
5
|
/**
|
|
6
6
|
* Dust tolerance for bundler retention, in raw token units.
|
|
@@ -45,13 +45,28 @@ function getBundlerAddresses(chainId, logger) {
|
|
|
45
45
|
* Uses net flow (inbound minus outbound) per (bundler address, token) pair.
|
|
46
46
|
* Bundler3 legitimately receives tokens as an intermediary (user → bundler3 →
|
|
47
47
|
* vault), so gross inbound would fire false positives. We only block positive
|
|
48
|
-
* net flow above `DUST_THRESHOLD` —
|
|
48
|
+
* net flow above `DUST_THRESHOLD` — value stuck in a bundler address. Negative
|
|
49
49
|
* net flow means the simulated bundle sweeps a pre-existing or state-overridden
|
|
50
50
|
* bundler balance. That is useful telemetry, but not current-bundle retention,
|
|
51
51
|
* so it is warned instead of raising a blacklist violation.
|
|
52
|
+
*
|
|
53
|
+
* **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
|
|
54
|
+
* from parsed `transfers` (log-derived, identical across backends). Native ETH
|
|
55
|
+
* emits no event log, so it can never appear in `transfers` on the Tenderly
|
|
56
|
+
* primary backend — Tenderly derives native moves into `assetChanges` instead.
|
|
57
|
+
* Native ETH is therefore read from `assetChanges`, the cross-backend source of
|
|
58
|
+
* truth for native value: Tenderly derives it from its trace, and
|
|
59
|
+
* `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
|
|
60
|
+
* this, native ETH stuck in bundler3 would pass the guard undetected on the
|
|
61
|
+
* Tenderly path (Cantina finding 1440).
|
|
62
|
+
*
|
|
63
|
+
* To avoid double-counting native ETH on `eth_simulateV1` — where the same
|
|
64
|
+
* native move exists both as a synthetic `ethAddress` transfer log *and* in the
|
|
65
|
+
* `assetChanges` derived from it — native transfer logs are only used as a
|
|
66
|
+
* fallback for bundler addresses that carry no native `assetChanges` entry.
|
|
52
67
|
*/
|
|
53
68
|
export function assertNoBundlerRetention(params) {
|
|
54
|
-
const { chainId, transfers, logger } = params;
|
|
69
|
+
const { chainId, transfers, assetChanges, logger } = params;
|
|
55
70
|
const bundlerAddresses = getBundlerAddresses(chainId, logger);
|
|
56
71
|
if (bundlerAddresses.size === 0)
|
|
57
72
|
return;
|
|
@@ -68,10 +83,29 @@ export function assertNoBundlerRetention(params) {
|
|
|
68
83
|
flow.set(key, { address, token, netChange: delta });
|
|
69
84
|
}
|
|
70
85
|
};
|
|
86
|
+
// Native ETH from `assetChanges` (authoritative, cross-backend). Track which
|
|
87
|
+
// bundlers already have a native entry here so the transfer-log pass below
|
|
88
|
+
// does not re-add the same native move on `eth_simulateV1`.
|
|
89
|
+
const nativeFromAssetChanges = new Set();
|
|
90
|
+
for (const { account, changes } of assetChanges) {
|
|
91
|
+
if (!bundlerAddresses.has(account))
|
|
92
|
+
continue;
|
|
93
|
+
for (const change of changes) {
|
|
94
|
+
if (change.token !== ethAddress)
|
|
95
|
+
continue;
|
|
96
|
+
recordFlow(account, ethAddress, change.diff);
|
|
97
|
+
nativeFromAssetChanges.add(account.toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ERC20 / WETH9 retention from transfer logs. Native ETH transfers (the
|
|
101
|
+
// `eth_simulateV1` synthetic sentinel) only backfill a bundler endpoint that
|
|
102
|
+
// is absent from `nativeFromAssetChanges`, so native is never summed on top
|
|
103
|
+
// of the `assetChanges` value already recorded for that address.
|
|
104
|
+
const usesAssetChangeNative = (t, addr) => t.token === ethAddress && nativeFromAssetChanges.has(addr.toLowerCase());
|
|
71
105
|
for (const t of transfers) {
|
|
72
|
-
if (bundlerAddresses.has(t.to))
|
|
106
|
+
if (bundlerAddresses.has(t.to) && !usesAssetChangeNative(t, t.to))
|
|
73
107
|
recordFlow(t.to, t.token, t.amount);
|
|
74
|
-
if (bundlerAddresses.has(t.from))
|
|
108
|
+
if (bundlerAddresses.has(t.from) && !usesAssetChangeNative(t, t.from))
|
|
75
109
|
recordFlow(t.from, t.token, -t.amount);
|
|
76
110
|
}
|
|
77
111
|
const entries = [...flow.values()];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morpho-org/evm-simulation",
|
|
3
3
|
"description": "EVM simulation engine for Morpho transactions.",
|
|
4
|
-
"version": "4.1.
|
|
4
|
+
"version": "4.1.2",
|
|
5
5
|
"author": "Morpho Association <contact@morpho.org>",
|
|
6
6
|
"contributors": [
|
|
7
7
|
"Rubilmax <rmilon@gmail.com>"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"type": "module",
|
|
21
21
|
"sideEffects": false,
|
|
22
|
-
"main": "
|
|
22
|
+
"main": "./lib/cjs/index.js",
|
|
23
23
|
"files": [
|
|
24
24
|
"lib"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"zod": "^4.4.3",
|
|
28
|
-
"@morpho-org/blue-sdk": "^6.3.
|
|
29
|
-
"@morpho-org/morpho-ts": "^2.
|
|
28
|
+
"@morpho-org/blue-sdk": "^6.3.1",
|
|
29
|
+
"@morpho-org/morpho-ts": "^2.8.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"viem": "^2.0.0"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"typescript": "^6.0.3",
|
|
36
36
|
"viem": "^2.54.2",
|
|
37
37
|
"vitest": "^4.1.9",
|
|
38
|
-
"@morpho-org/test": "^2.8.
|
|
38
|
+
"@morpho-org/test": "^2.8.2"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"prepublish": "$npm_execpath build",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"test": "vitest --root ../.. --project evm-simulation",
|
|
46
46
|
"test:fork": "dotenv -- vitest --root ../.. --project evm-simulation-fork"
|
|
47
47
|
},
|
|
48
|
+
"types": "./lib/esm/index.d.ts",
|
|
48
49
|
"exports": {
|
|
49
50
|
".": {
|
|
50
51
|
"types": "./lib/esm/index.d.ts",
|
package/src/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Public fns (via feature-folder barrels)
|
|
2
|
-
|
|
3
|
-
// Errors (for instanceof checks by consumers)
|
|
4
|
-
export {
|
|
5
|
-
BlacklistViolationError,
|
|
6
|
-
ExternalServiceError,
|
|
7
|
-
SimulationPackageError,
|
|
8
|
-
SimulationRevertedError,
|
|
9
|
-
SimulationValidationError,
|
|
10
|
-
UnsupportedChainError,
|
|
11
|
-
} from "./errors.js";
|
|
12
|
-
export { simulate } from "./simulate/index.js";
|
|
13
|
-
// Types
|
|
14
|
-
export type {
|
|
15
|
-
AccountAssetChanges,
|
|
16
|
-
AssetChange,
|
|
17
|
-
ChainSimulationConfig,
|
|
18
|
-
RawLog,
|
|
19
|
-
SimulateParams,
|
|
20
|
-
SimulationAuthorization,
|
|
21
|
-
SimulationCall,
|
|
22
|
-
SimulationConfig,
|
|
23
|
-
SimulationLogger,
|
|
24
|
-
SimulationResult,
|
|
25
|
-
SimulationTransaction,
|
|
26
|
-
TenderlyRpcConfig,
|
|
27
|
-
Transfer,
|
|
28
|
-
} from "./types.js";
|