@morpho-org/evm-simulation 4.2.0 → 5.0.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.
Files changed (160) hide show
  1. package/README.md +109 -17
  2. package/lib/cjs/authorizations.d.ts +95 -0
  3. package/lib/cjs/authorizations.js +2 -0
  4. package/lib/cjs/errors.d.ts +207 -13
  5. package/lib/cjs/errors.js +197 -13
  6. package/lib/cjs/index.d.ts +10 -2
  7. package/lib/cjs/index.js +19 -3
  8. package/lib/cjs/limits.d.ts +112 -0
  9. package/lib/cjs/limits.js +50 -0
  10. package/lib/cjs/params.d.ts +25 -0
  11. package/lib/cjs/params.js +5 -0
  12. package/lib/cjs/requirements/index.d.ts +1 -0
  13. package/lib/cjs/requirements/index.js +5 -0
  14. package/lib/cjs/requirements/requirements.d.ts +64 -0
  15. package/lib/cjs/requirements/requirements.js +149 -0
  16. package/lib/cjs/result.d.ts +49 -0
  17. package/lib/cjs/result.js +2 -0
  18. package/lib/cjs/simulate/asset-changes.d.ts +7 -5
  19. package/lib/cjs/simulate/asset-changes.js +6 -5
  20. package/lib/cjs/simulate/backends/client.d.ts +16 -0
  21. package/lib/cjs/simulate/backends/client.js +27 -0
  22. package/lib/cjs/simulate/backends/eth-simulate-v1.d.ts +66 -20
  23. package/lib/cjs/simulate/backends/eth-simulate-v1.js +172 -98
  24. package/lib/cjs/simulate/backends/index.d.ts +1 -2
  25. package/lib/cjs/simulate/backends/index.js +3 -4
  26. package/lib/cjs/simulate/backends/parse-response.d.ts +69 -0
  27. package/lib/cjs/simulate/backends/parse-response.js +168 -0
  28. package/lib/cjs/simulate/backends/resolve-assets.d.ts +26 -0
  29. package/lib/cjs/simulate/backends/resolve-assets.js +111 -0
  30. package/lib/cjs/simulate/backends/resolve-pinned-block.d.ts +33 -0
  31. package/lib/cjs/simulate/backends/resolve-pinned-block.js +46 -0
  32. package/lib/cjs/simulate/context.d.ts +14 -0
  33. package/lib/cjs/simulate/context.js +2 -0
  34. package/lib/cjs/simulate/measurement-plan.d.ts +29 -0
  35. package/lib/cjs/simulate/measurement-plan.js +115 -0
  36. package/lib/cjs/simulate/parsing/transfers.d.ts +4 -3
  37. package/lib/cjs/simulate/parsing/transfers.js +4 -6
  38. package/lib/cjs/simulate/pipeline/bundles-retention.d.ts +7 -10
  39. package/lib/cjs/simulate/pipeline/bundles-retention.js +11 -14
  40. package/lib/cjs/simulate/pipeline/resolve-chain.d.ts +5 -11
  41. package/lib/cjs/simulate/pipeline/resolve-chain.js +5 -10
  42. package/lib/cjs/simulate/plan/plan-execution.d.ts +58 -0
  43. package/lib/cjs/simulate/plan/plan-execution.js +75 -0
  44. package/lib/cjs/simulate/prepare.d.ts +30 -0
  45. package/lib/cjs/simulate/prepare.js +80 -0
  46. package/lib/cjs/simulate/request/index.d.ts +1 -0
  47. package/lib/cjs/simulate/request/index.js +5 -0
  48. package/lib/cjs/simulate/request/parse-request.d.ts +44 -0
  49. package/lib/cjs/simulate/request/parse-request.js +759 -0
  50. package/lib/cjs/simulate/result.d.ts +19 -0
  51. package/lib/cjs/simulate/result.js +29 -0
  52. package/lib/cjs/simulate/run-simulation.d.ts +18 -0
  53. package/lib/cjs/simulate/run-simulation.js +151 -0
  54. package/lib/cjs/simulate/simulate.d.ts +100 -50
  55. package/lib/cjs/simulate/simulate.js +99 -81
  56. package/lib/cjs/simulate/slippage.d.ts +18 -0
  57. package/lib/cjs/simulate/slippage.js +41 -0
  58. package/lib/cjs/simulate/state/contract.d.ts +19 -0
  59. package/lib/cjs/simulate/state/contract.js +2 -0
  60. package/lib/cjs/simulate/state/read-state.d.ts +60 -0
  61. package/lib/cjs/simulate/state/read-state.js +148 -0
  62. package/lib/cjs/simulate/verify-slippage.d.ts +24 -0
  63. package/lib/cjs/simulate/verify-slippage.js +85 -0
  64. package/lib/cjs/types.d.ts +35 -95
  65. package/lib/esm/authorizations.d.ts +95 -0
  66. package/lib/esm/authorizations.js +1 -0
  67. package/lib/esm/errors.d.ts +207 -13
  68. package/lib/esm/errors.js +189 -12
  69. package/lib/esm/index.d.ts +10 -2
  70. package/lib/esm/index.js +4 -1
  71. package/lib/esm/limits.d.ts +112 -0
  72. package/lib/esm/limits.js +47 -0
  73. package/lib/esm/params.d.ts +25 -0
  74. package/lib/esm/params.js +2 -0
  75. package/lib/esm/requirements/index.d.ts +1 -0
  76. package/lib/esm/requirements/index.js +1 -0
  77. package/lib/esm/requirements/requirements.d.ts +64 -0
  78. package/lib/esm/requirements/requirements.js +146 -0
  79. package/lib/esm/result.d.ts +49 -0
  80. package/lib/esm/result.js +1 -0
  81. package/lib/esm/simulate/asset-changes.d.ts +7 -5
  82. package/lib/esm/simulate/asset-changes.js +6 -5
  83. package/lib/esm/simulate/backends/client.d.ts +16 -0
  84. package/lib/esm/simulate/backends/client.js +24 -0
  85. package/lib/esm/simulate/backends/eth-simulate-v1.d.ts +66 -20
  86. package/lib/esm/simulate/backends/eth-simulate-v1.js +170 -99
  87. package/lib/esm/simulate/backends/index.d.ts +1 -2
  88. package/lib/esm/simulate/backends/index.js +1 -2
  89. package/lib/esm/simulate/backends/parse-response.d.ts +69 -0
  90. package/lib/esm/simulate/backends/parse-response.js +165 -0
  91. package/lib/esm/simulate/backends/resolve-assets.d.ts +26 -0
  92. package/lib/esm/simulate/backends/resolve-assets.js +108 -0
  93. package/lib/esm/simulate/backends/resolve-pinned-block.d.ts +33 -0
  94. package/lib/esm/simulate/backends/resolve-pinned-block.js +43 -0
  95. package/lib/esm/simulate/context.d.ts +14 -0
  96. package/lib/esm/simulate/context.js +1 -0
  97. package/lib/esm/simulate/measurement-plan.d.ts +29 -0
  98. package/lib/esm/simulate/measurement-plan.js +112 -0
  99. package/lib/esm/simulate/parsing/transfers.d.ts +4 -3
  100. package/lib/esm/simulate/parsing/transfers.js +4 -6
  101. package/lib/esm/simulate/pipeline/bundles-retention.d.ts +7 -10
  102. package/lib/esm/simulate/pipeline/bundles-retention.js +11 -14
  103. package/lib/esm/simulate/pipeline/resolve-chain.d.ts +5 -11
  104. package/lib/esm/simulate/pipeline/resolve-chain.js +5 -10
  105. package/lib/esm/simulate/plan/plan-execution.d.ts +58 -0
  106. package/lib/esm/simulate/plan/plan-execution.js +72 -0
  107. package/lib/esm/simulate/prepare.d.ts +30 -0
  108. package/lib/esm/simulate/prepare.js +77 -0
  109. package/lib/esm/simulate/request/index.d.ts +1 -0
  110. package/lib/esm/simulate/request/index.js +1 -0
  111. package/lib/esm/simulate/request/parse-request.d.ts +44 -0
  112. package/lib/esm/simulate/request/parse-request.js +756 -0
  113. package/lib/esm/simulate/result.d.ts +19 -0
  114. package/lib/esm/simulate/result.js +26 -0
  115. package/lib/esm/simulate/run-simulation.d.ts +18 -0
  116. package/lib/esm/simulate/run-simulation.js +148 -0
  117. package/lib/esm/simulate/simulate.d.ts +100 -50
  118. package/lib/esm/simulate/simulate.js +99 -81
  119. package/lib/esm/simulate/slippage.d.ts +18 -0
  120. package/lib/esm/simulate/slippage.js +38 -0
  121. package/lib/esm/simulate/state/contract.d.ts +19 -0
  122. package/lib/esm/simulate/state/contract.js +1 -0
  123. package/lib/esm/simulate/state/read-state.d.ts +60 -0
  124. package/lib/esm/simulate/state/read-state.js +144 -0
  125. package/lib/esm/simulate/verify-slippage.d.ts +24 -0
  126. package/lib/esm/simulate/verify-slippage.js +82 -0
  127. package/lib/esm/types.d.ts +35 -95
  128. package/package.json +7 -6
  129. package/lib/cjs/simulate/authorizations/index.d.ts +0 -2
  130. package/lib/cjs/simulate/authorizations/index.js +0 -7
  131. package/lib/cjs/simulate/authorizations/resolve.d.ts +0 -9
  132. package/lib/cjs/simulate/authorizations/resolve.js +0 -29
  133. package/lib/cjs/simulate/authorizations/validate.d.ts +0 -6
  134. package/lib/cjs/simulate/authorizations/validate.js +0 -23
  135. package/lib/cjs/simulate/backends/tenderly-rpc.d.ts +0 -23
  136. package/lib/cjs/simulate/backends/tenderly-rpc.js +0 -234
  137. package/lib/cjs/simulate/pipeline/build-simulation-txs.d.ts +0 -17
  138. package/lib/cjs/simulate/pipeline/build-simulation-txs.js +0 -50
  139. package/lib/cjs/simulate/pipeline/execute-simulation.d.ts +0 -22
  140. package/lib/cjs/simulate/pipeline/execute-simulation.js +0 -82
  141. package/lib/cjs/simulate/pipeline/index.d.ts +0 -4
  142. package/lib/cjs/simulate/pipeline/index.js +0 -11
  143. package/lib/cjs/simulate/pipeline/validate-input.d.ts +0 -10
  144. package/lib/cjs/simulate/pipeline/validate-input.js +0 -55
  145. package/lib/esm/simulate/authorizations/index.d.ts +0 -2
  146. package/lib/esm/simulate/authorizations/index.js +0 -2
  147. package/lib/esm/simulate/authorizations/resolve.d.ts +0 -9
  148. package/lib/esm/simulate/authorizations/resolve.js +0 -26
  149. package/lib/esm/simulate/authorizations/validate.d.ts +0 -6
  150. package/lib/esm/simulate/authorizations/validate.js +0 -20
  151. package/lib/esm/simulate/backends/tenderly-rpc.d.ts +0 -23
  152. package/lib/esm/simulate/backends/tenderly-rpc.js +0 -231
  153. package/lib/esm/simulate/pipeline/build-simulation-txs.d.ts +0 -17
  154. package/lib/esm/simulate/pipeline/build-simulation-txs.js +0 -47
  155. package/lib/esm/simulate/pipeline/execute-simulation.d.ts +0 -22
  156. package/lib/esm/simulate/pipeline/execute-simulation.js +0 -79
  157. package/lib/esm/simulate/pipeline/index.d.ts +0 -4
  158. package/lib/esm/simulate/pipeline/index.js +0 -4
  159. package/lib/esm/simulate/pipeline/validate-input.d.ts +0 -10
  160. package/lib/esm/simulate/pipeline/validate-input.js +0 -52
@@ -0,0 +1,108 @@
1
+ import { blueAbi } from "@morpho-org/morpho-sdk/abis";
2
+ import { BaseError, ContractFunctionRevertedError, ExecutionRevertedError, erc4626Abi, HttpRequestError, isAddressEqual, RpcError, RpcRequestError, TimeoutError, zeroAddress, } from "viem";
3
+ import { readContract } from "viem/actions";
4
+ import { ExternalServiceError, MissingVerificationEvidenceError, } from "../../errors.js";
5
+ import { operationMeasurementPlan } from "../measurement-plan.js";
6
+ /**
7
+ * Resolve underlying tokens only when an asset quote lacks an explicit token.
8
+ * Share-only quotes need no RPC reads; vault versions and factories are not discovered.
9
+ * @param params - Caller operations, Morpho address, client, request context, and pinned state block.
10
+ * @returns Operations with the token addresses required by their asset quotes.
11
+ * @throws {ExternalServiceError} For transport failures during a required metadata read.
12
+ * @throws {MissingVerificationEvidenceError} For reverted, malformed, or empty metadata.
13
+ * @internal
14
+ */
15
+ export async function resolveAssets(params) {
16
+ const { client, morpho, operations, chainId, mode, blockNumber } = params;
17
+ const tokens = new Map();
18
+ const resolved = [];
19
+ for (const limit of operations) {
20
+ const plan = operationMeasurementPlan(limit);
21
+ const result = { limit };
22
+ for (const field of ["assetsPaid", "assetsReceived"]) {
23
+ if (limit.quote[field] === undefined)
24
+ continue;
25
+ const explicit = field === "assetsPaid" ? limit.assetPaid : limit.assetReceived;
26
+ if (explicit !== undefined) {
27
+ result[field] = explicit;
28
+ continue;
29
+ }
30
+ const source = plan[field];
31
+ const key = source.type === "market"
32
+ ? `market:${source.marketId.toLowerCase()}`
33
+ : `vault:${source.vault.toLowerCase()}`;
34
+ let pair = tokens.get(key);
35
+ if (pair === undefined) {
36
+ try {
37
+ if (source.type === "market") {
38
+ const values = await readContract(client, {
39
+ address: morpho,
40
+ abi: blueAbi,
41
+ functionName: "idToMarketParams",
42
+ args: [source.marketId],
43
+ blockNumber,
44
+ });
45
+ pair = [values[0], values[1]];
46
+ }
47
+ else {
48
+ const asset = await readContract(client, {
49
+ address: source.vault,
50
+ abi: erc4626Abi,
51
+ functionName: "asset",
52
+ blockNumber,
53
+ });
54
+ pair = [asset, asset];
55
+ }
56
+ }
57
+ catch (cause) {
58
+ const reverted = cause instanceof BaseError &&
59
+ cause.walk((error) => error instanceof ContractFunctionRevertedError ||
60
+ error instanceof ExecutionRevertedError) !== null;
61
+ // Revert-shaped chains (code 3, or -32000 "execution reverted")
62
+ // can nest an RpcRequestError, so a revert must win over the
63
+ // transport match.
64
+ if (!reverted &&
65
+ ((cause instanceof BaseError &&
66
+ cause.walk((error) => error instanceof HttpRequestError ||
67
+ error instanceof TimeoutError ||
68
+ error instanceof RpcRequestError ||
69
+ error instanceof RpcError ||
70
+ (error instanceof Error && error.name === "AbortError")) !== null) ||
71
+ (cause instanceof Error && cause.name === "AbortError"))) {
72
+ throw new ExternalServiceError("Cannot resolve the quoted asset because its metadata request failed. Check the RPC endpoint.", { cause });
73
+ }
74
+ throw new MissingVerificationEvidenceError(`Cannot resolve verification evidence for "${limit.type}" asset source "${key}".`, {
75
+ cause,
76
+ context: {
77
+ stage: "verification",
78
+ chainId,
79
+ mode,
80
+ blockNumber,
81
+ ...plan.subject,
82
+ field,
83
+ },
84
+ });
85
+ }
86
+ tokens.set(key, pair);
87
+ }
88
+ const token = source.type === "market"
89
+ ? pair[source.asset === "loan" ? 0 : 1]
90
+ : pair[0];
91
+ if (isAddressEqual(token, zeroAddress)) {
92
+ throw new MissingVerificationEvidenceError(`Cannot resolve verification evidence for "${limit.type}" asset source "${key}": the resolved token address is zero.`, {
93
+ context: {
94
+ stage: "verification",
95
+ chainId,
96
+ mode,
97
+ blockNumber,
98
+ ...plan.subject,
99
+ field,
100
+ },
101
+ });
102
+ }
103
+ result[field] = token;
104
+ }
105
+ resolved.push(result);
106
+ }
107
+ return resolved;
108
+ }
@@ -0,0 +1,33 @@
1
+ import type { BlockTag, Client, Hex } from "viem";
2
+ /** One concrete canonical block: the state anchor for every pinned read. @internal */
3
+ export interface PinnedBlock {
4
+ readonly number: bigint;
5
+ readonly hash: Hex;
6
+ readonly timestamp: bigint;
7
+ }
8
+ /**
9
+ * Resolve the requested state block exactly once so `latest` cannot drift.
10
+ *
11
+ * Every downstream read and the `eth_simulateV1` call pin this number; the
12
+ * execution boundary re-fetches the hash afterwards to detect a reorg that
13
+ * swapped the block mid-flight.
14
+ *
15
+ * @param params - Resolution parameters.
16
+ * @param params.client - Shared simulation client (created by
17
+ * {@link createSimulationClient}).
18
+ * @param params.blockNumber - Requested block number or tag; defaults to
19
+ * `latest`.
20
+ * @param params.signal - Optional pipeline abort signal; checked around the
21
+ * request so an aborted pipeline never produces a pin.
22
+ * @returns The resolved {@link PinnedBlock}.
23
+ * @throws {ExternalServiceError} For transport failures, timeouts, or an
24
+ * aborted signal.
25
+ * @throws {InvalidSimulationResponseError} When the node returns a block
26
+ * without a number or hash.
27
+ * @internal
28
+ */
29
+ export declare function resolvePinnedBlock(params: {
30
+ readonly client: Client;
31
+ readonly blockNumber?: bigint | BlockTag;
32
+ readonly signal?: AbortSignal;
33
+ }): Promise<PinnedBlock>;
@@ -0,0 +1,43 @@
1
+ import { getBlock } from "viem/actions";
2
+ import { InvalidSimulationResponseError } from "../../errors.js";
3
+ import { rpc } from "./eth-simulate-v1.js";
4
+ /**
5
+ * Resolve the requested state block exactly once so `latest` cannot drift.
6
+ *
7
+ * Every downstream read and the `eth_simulateV1` call pin this number; the
8
+ * execution boundary re-fetches the hash afterwards to detect a reorg that
9
+ * swapped the block mid-flight.
10
+ *
11
+ * @param params - Resolution parameters.
12
+ * @param params.client - Shared simulation client (created by
13
+ * {@link createSimulationClient}).
14
+ * @param params.blockNumber - Requested block number or tag; defaults to
15
+ * `latest`.
16
+ * @param params.signal - Optional pipeline abort signal; checked around the
17
+ * request so an aborted pipeline never produces a pin.
18
+ * @returns The resolved {@link PinnedBlock}.
19
+ * @throws {ExternalServiceError} For transport failures, timeouts, or an
20
+ * aborted signal.
21
+ * @throws {InvalidSimulationResponseError} When the node returns a block
22
+ * without a number or hash.
23
+ * @internal
24
+ */
25
+ export async function resolvePinnedBlock(params) {
26
+ const { client, blockNumber, signal } = params;
27
+ const block = await rpc("eth_getBlock", async () => {
28
+ signal?.throwIfAborted();
29
+ const resolved = await getBlock(client, typeof blockNumber === "bigint"
30
+ ? { blockNumber }
31
+ : { blockTag: blockNumber ?? "latest" });
32
+ signal?.throwIfAborted();
33
+ return resolved;
34
+ });
35
+ if (block.number === null || block.hash === null) {
36
+ throw new InvalidSimulationResponseError("eth_getBlock returned a block without number or hash. Check that the endpoint resolved the requested state block.");
37
+ }
38
+ return {
39
+ number: block.number,
40
+ hash: block.hash,
41
+ timestamp: block.timestamp,
42
+ };
43
+ }
@@ -0,0 +1,14 @@
1
+ import type { Address } from "viem";
2
+ import type { OperationLimit } from "../limits.js";
3
+ import type { SimulationMode } from "../params.js";
4
+ import type { ExecutionBlock } from "./backends/parse-response.js";
5
+ /** Explicit simulation context shared by the checker and result assembly. @internal */
6
+ export interface CheckContext {
7
+ readonly chainId: number;
8
+ readonly mode: SimulationMode;
9
+ readonly block: ExecutionBlock;
10
+ readonly owner: Address;
11
+ readonly limits: {
12
+ readonly operations: readonly OperationLimit[];
13
+ };
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import type { MarketId } from "@morpho-org/blue-sdk";
2
+ import type { Address } from "viem";
3
+ import type { OperationLimit, SimulationOperationSubject } from "../limits.js";
4
+ type AssetMeasurementSource = {
5
+ readonly type: "market";
6
+ readonly marketId: MarketId;
7
+ readonly asset: "loan" | "collateral";
8
+ } | {
9
+ readonly type: "vault";
10
+ readonly vault: Address;
11
+ };
12
+ type ShareMeasurementSource = {
13
+ readonly type: "position";
14
+ readonly marketId: MarketId;
15
+ readonly shares: "supplyShares" | "borrowShares";
16
+ } | {
17
+ readonly type: "balance";
18
+ readonly token: Address;
19
+ };
20
+ interface OperationMeasurementPlan {
21
+ readonly subject: SimulationOperationSubject;
22
+ readonly assetsPaid: AssetMeasurementSource;
23
+ readonly assetsReceived: AssetMeasurementSource;
24
+ readonly sharesMinted?: ShareMeasurementSource;
25
+ readonly sharesBurned?: ShareMeasurementSource;
26
+ }
27
+ /** Describe each quote field's evidence source with an exhaustive operation switch. @internal */
28
+ export declare function operationMeasurementPlan(limit: OperationLimit): OperationMeasurementPlan;
29
+ export {};
@@ -0,0 +1,112 @@
1
+ const market = (marketId, asset) => ({ type: "market", marketId, asset });
2
+ const position = (marketId, shares) => ({ type: "position", marketId, shares });
3
+ const balance = (token) => ({
4
+ type: "balance",
5
+ token,
6
+ });
7
+ const vault = (address) => ({
8
+ type: "vault",
9
+ vault: address,
10
+ });
11
+ /** Describe each quote field's evidence source with an exhaustive operation switch. @internal */
12
+ export function operationMeasurementPlan(limit) {
13
+ switch (limit.type) {
14
+ case "blueSupply":
15
+ case "blueWithdraw": {
16
+ const shares = position(limit.marketId, "supplyShares");
17
+ return {
18
+ subject: { operation: limit.type, marketId: limit.marketId },
19
+ assetsPaid: market(limit.marketId, "loan"),
20
+ assetsReceived: market(limit.marketId, "loan"),
21
+ sharesMinted: shares,
22
+ sharesBurned: shares,
23
+ };
24
+ }
25
+ case "blueSupplyCollateral":
26
+ case "blueWithdrawCollateral":
27
+ return {
28
+ subject: { operation: limit.type, marketId: limit.marketId },
29
+ assetsPaid: market(limit.marketId, "collateral"),
30
+ assetsReceived: market(limit.marketId, "collateral"),
31
+ };
32
+ case "blueBorrow":
33
+ case "blueRepay": {
34
+ const shares = position(limit.marketId, "borrowShares");
35
+ return {
36
+ subject: { operation: limit.type, marketId: limit.marketId },
37
+ assetsPaid: market(limit.marketId, "loan"),
38
+ assetsReceived: market(limit.marketId, "loan"),
39
+ sharesMinted: shares,
40
+ sharesBurned: shares,
41
+ };
42
+ }
43
+ case "blueSupplyCollateralBorrow": {
44
+ const shares = position(limit.marketId, "borrowShares");
45
+ return {
46
+ subject: { operation: limit.type, marketId: limit.marketId },
47
+ assetsPaid: market(limit.marketId, "collateral"),
48
+ assetsReceived: market(limit.marketId, "loan"),
49
+ sharesMinted: shares,
50
+ sharesBurned: shares,
51
+ };
52
+ }
53
+ case "blueRepayWithdrawCollateral": {
54
+ const shares = position(limit.marketId, "borrowShares");
55
+ return {
56
+ subject: { operation: limit.type, marketId: limit.marketId },
57
+ assetsPaid: market(limit.marketId, "loan"),
58
+ assetsReceived: market(limit.marketId, "collateral"),
59
+ sharesMinted: shares,
60
+ sharesBurned: shares,
61
+ };
62
+ }
63
+ case "blueRefinance":
64
+ return {
65
+ subject: {
66
+ operation: limit.type,
67
+ sourceMarketId: limit.sourceMarketId,
68
+ targetMarketId: limit.targetMarketId,
69
+ },
70
+ assetsPaid: market(limit.sourceMarketId, "loan"),
71
+ assetsReceived: market(limit.sourceMarketId, "loan"),
72
+ sharesMinted: position(limit.targetMarketId, "borrowShares"),
73
+ sharesBurned: position(limit.sourceMarketId, "borrowShares"),
74
+ };
75
+ case "vaultV1Deposit":
76
+ case "vaultV2Deposit":
77
+ case "vaultV1Withdraw":
78
+ case "vaultV2Withdraw":
79
+ case "vaultV1Redeem":
80
+ case "vaultV2Redeem":
81
+ case "vaultV2ForceWithdraw":
82
+ case "vaultV2ForceRedeem":
83
+ case "vaultV1InKindRedeem":
84
+ case "vaultV2InKindRedeem":
85
+ return {
86
+ subject: {
87
+ operation: limit.type,
88
+ vault: limit.vault,
89
+ },
90
+ assetsPaid: vault(limit.vault),
91
+ assetsReceived: vault(limit.vault),
92
+ sharesMinted: balance(limit.vault),
93
+ sharesBurned: balance(limit.vault),
94
+ };
95
+ case "vaultV1MigrateToV2":
96
+ return {
97
+ subject: {
98
+ operation: limit.type,
99
+ sourceVault: limit.sourceVault,
100
+ targetVault: limit.targetVault,
101
+ },
102
+ assetsPaid: vault(limit.sourceVault),
103
+ assetsReceived: vault(limit.sourceVault),
104
+ sharesMinted: balance(limit.targetVault),
105
+ sharesBurned: balance(limit.sourceVault),
106
+ };
107
+ default: {
108
+ const exhaustive = limit;
109
+ return exhaustive;
110
+ }
111
+ }
112
+ }
@@ -1,7 +1,10 @@
1
1
  import { type Address } from "viem";
2
2
  import type { RawCall, SimulationLogger, Transfer } from "../../types.js";
3
+ /** `keccak256("Transfer(address,address,uint256)")` — ERC-20 Transfer event topic. @internal */
3
4
  export declare const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
5
+ /** `keccak256("Withdrawal(address,uint256)")` — WETH9 Withdrawal event topic. @internal */
4
6
  export declare const WITHDRAWAL_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
7
+ /** `keccak256("Deposit(address,uint256)")` — WETH9 Deposit event topic. @internal */
5
8
  export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
6
9
  /**
7
10
  * Parse raw EVM logs into individual Transfer events.
@@ -23,9 +26,7 @@ export declare const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d
23
26
  * those made through internal calls (e.g. a `WETH.withdraw` refund) — are
24
27
  * synthesized as `Transfer` events emitted from the native sentinel
25
28
  * `0xeee…eee`. These are parsed like any ERC20 transfer, with `token`
26
- * collapsed to viem's `ethAddress` so native deltas key consistently across
27
- * backends. Tenderly does not emit these synthetic logs (it derives native ETH
28
- * separately), so this path is inert there.
29
+ * collapsed to viem's `ethAddress` so native deltas key consistently.
29
30
  *
30
31
  * **WETH9 dedup assumption — canonical atomic emission.** Dedup for the
31
32
  * registered wrapped-native token is scoped to the same tx: a zero-address
@@ -1,10 +1,10 @@
1
1
  import { getAddress, isAddressEqual, zeroAddress, zeroHash, } from "viem";
2
2
  import { normalizeAssetToken } from "../asset-changes.js";
3
- // keccak256("Transfer(address,address,uint256)") — ERC-20 transfer event
3
+ /** `keccak256("Transfer(address,address,uint256)")` — ERC-20 Transfer event topic. @internal */
4
4
  export const TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
5
- // keccak256("Withdrawal(address,uint256)") — WETH9 unwrap event
5
+ /** `keccak256("Withdrawal(address,uint256)")` — WETH9 Withdrawal event topic. @internal */
6
6
  export const WITHDRAWAL_TOPIC = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65";
7
- // keccak256("Deposit(address,uint256)") — WETH9 wrap event
7
+ /** `keccak256("Deposit(address,uint256)")` — WETH9 Deposit event topic. @internal */
8
8
  export const DEPOSIT_TOPIC = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c";
9
9
  const TOPIC_HEX_LENGTH = 66; // "0x" + 32 bytes
10
10
  const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
@@ -28,9 +28,7 @@ const UINT256_HEX_LENGTH = 66; // "0x" + 32 bytes
28
28
  * those made through internal calls (e.g. a `WETH.withdraw` refund) — are
29
29
  * synthesized as `Transfer` events emitted from the native sentinel
30
30
  * `0xeee…eee`. These are parsed like any ERC20 transfer, with `token`
31
- * collapsed to viem's `ethAddress` so native deltas key consistently across
32
- * backends. Tenderly does not emit these synthetic logs (it derives native ETH
33
- * separately), so this path is inert there.
31
+ * collapsed to viem's `ethAddress` so native deltas key consistently.
34
32
  *
35
33
  * **WETH9 dedup assumption — canonical atomic emission.** Dedup for the
36
34
  * registered wrapped-native token is scoped to the same tx: a zero-address
@@ -20,14 +20,11 @@ interface AssertNoBundlesRetentionParams {
20
20
  * violation.
21
21
  *
22
22
  * **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
23
- * from parsed `transfers` (log-derived, identical across backends). Native ETH
24
- * emits no event log, so it can never appear in `transfers` on the Tenderly
25
- * primary backend — Tenderly derives native moves into `assetChanges` instead.
26
- * Native ETH is therefore read from `assetChanges`, the cross-backend source of
27
- * truth for native value: Tenderly derives it from its trace, and
28
- * `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
29
- * this, native ETH stuck in a bundles contract would pass the guard undetected
30
- * on the Tenderly path (Cantina finding 1440).
23
+ * from parsed `transfers` (log-derived). Native ETH emits no event log of its
24
+ * own, so it is read from `assetChanges`, the source of truth for native value,
25
+ * which `eth_simulateV1` derives from the synthetic `traceTransfers` logs.
26
+ * Without this, native ETH stuck in a bundles contract would pass the guard
27
+ * undetected (Cantina finding 1440).
31
28
  *
32
29
  * To avoid double-counting native ETH on `eth_simulateV1` — where the same
33
30
  * native move exists both as a synthetic `ethAddress` transfer log *and* in the
@@ -41,8 +38,8 @@ interface AssertNoBundlesRetentionParams {
41
38
  * neither are skipped (a `logger.warn` records the skip).
42
39
  * @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
43
40
  * `eth_simulateV1` synthetic native sentinel) scanned for net retention.
44
- * @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
45
- * source of truth for native value stuck in a restricted contract.
41
+ * @param params.assetChanges - Per-account native-ETH deltas, the source of
42
+ * truth for native value stuck in a restricted contract.
46
43
  * @param params.logger - Optional logger. Receives retention-check skip warnings
47
44
  * and pre-existing-balance sweep telemetry (net-negative flow).
48
45
  * @returns Nothing. Returns silently when no restricted contract retains value
@@ -27,14 +27,11 @@ const DUST_THRESHOLD = 100n;
27
27
  * violation.
28
28
  *
29
29
  * **Two flow sources, one per asset class.** ERC20 / WETH9 retention is read
30
- * from parsed `transfers` (log-derived, identical across backends). Native ETH
31
- * emits no event log, so it can never appear in `transfers` on the Tenderly
32
- * primary backend — Tenderly derives native moves into `assetChanges` instead.
33
- * Native ETH is therefore read from `assetChanges`, the cross-backend source of
34
- * truth for native value: Tenderly derives it from its trace, and
35
- * `eth_simulateV1` derives it from the synthetic `traceTransfers` logs. Without
36
- * this, native ETH stuck in a bundles contract would pass the guard undetected
37
- * on the Tenderly path (Cantina finding 1440).
30
+ * from parsed `transfers` (log-derived). Native ETH emits no event log of its
31
+ * own, so it is read from `assetChanges`, the source of truth for native value,
32
+ * which `eth_simulateV1` derives from the synthetic `traceTransfers` logs.
33
+ * Without this, native ETH stuck in a bundles contract would pass the guard
34
+ * undetected (Cantina finding 1440).
38
35
  *
39
36
  * To avoid double-counting native ETH on `eth_simulateV1` — where the same
40
37
  * native move exists both as a synthetic `ethAddress` transfer log *and* in the
@@ -48,8 +45,8 @@ const DUST_THRESHOLD = 100n;
48
45
  * neither are skipped (a `logger.warn` records the skip).
49
46
  * @param params.transfers - Parsed ERC20 / WETH9 transfer flows (plus the
50
47
  * `eth_simulateV1` synthetic native sentinel) scanned for net retention.
51
- * @param params.assetChanges - Per-account native-ETH deltas, the cross-backend
52
- * source of truth for native value stuck in a restricted contract.
48
+ * @param params.assetChanges - Per-account native-ETH deltas, the source of
49
+ * truth for native value stuck in a restricted contract.
53
50
  * @param params.logger - Optional logger. Receives retention-check skip warnings
54
51
  * and pre-existing-balance sweep telemetry (net-negative flow).
55
52
  * @returns Nothing. Returns silently when no restricted contract retains value
@@ -104,9 +101,9 @@ export function assertNoBundlesRetention(params) {
104
101
  flow.set(key, { address, token, netChange: delta });
105
102
  }
106
103
  };
107
- // Native ETH from `assetChanges` (authoritative, cross-backend). Track which
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`.
104
+ // Native ETH from `assetChanges` (authoritative). Track which restricted
105
+ // contracts already have a native entry here so the transfer-log pass below
106
+ // does not re-add the same native move on `eth_simulateV1`.
110
107
  const nativeFromAssetChanges = new Set();
111
108
  for (const { account, changes } of assetChanges) {
112
109
  if (!restrictedAddresses.has(account))
@@ -145,7 +142,7 @@ export function assertNoBundlesRetention(params) {
145
142
  throw new BlacklistViolationError("Simulation detected asset transfers retained by restricted bundles contracts", retained.map((e) => ({
146
143
  address: e.address,
147
144
  token: e.token,
148
- netRetained: e.netChange.toString(),
145
+ netRetained: e.netChange,
149
146
  })));
150
147
  }
151
148
  }
@@ -1,15 +1,9 @@
1
- import type { SimulationConfig, TenderlyRpcConfig } from "../../types.js";
2
- interface ChainCapability {
3
- tenderlyRpc?: TenderlyRpcConfig;
4
- simulateV1Url?: string;
5
- }
1
+ import type { ChainSimulationConfig, SimulationConfig } from "../../types.js";
6
2
  /**
7
3
  * Stage 2 of the simulate() pipeline.
8
4
  *
9
- * Looks up the per-chain `ChainSimulationConfig` and exposes whichever
10
- * backends were configured. Throws `UnsupportedChainError` when the chain is
11
- * absent from the map or has neither backend wired (defensive — the type
12
- * already enforces at least one at construction).
5
+ * Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
6
+ * when the chain is absent from the map or has no `simulateV1Url` (defensive —
7
+ * the type already requires it at construction).
13
8
  */
14
- export declare function resolveChain(config: SimulationConfig, chainId: number): ChainCapability;
15
- export {};
9
+ export declare function resolveChain(config: SimulationConfig, chainId: number): ChainSimulationConfig;
@@ -2,18 +2,13 @@ import { UnsupportedChainError } from "../../errors.js";
2
2
  /**
3
3
  * Stage 2 of the simulate() pipeline.
4
4
  *
5
- * Looks up the per-chain `ChainSimulationConfig` and exposes whichever
6
- * backends were configured. Throws `UnsupportedChainError` when the chain is
7
- * absent from the map or has neither backend wired (defensive — the type
8
- * already enforces at least one at construction).
5
+ * Looks up the per-chain `ChainSimulationConfig`. Throws `UnsupportedChainError`
6
+ * when the chain is absent from the map or has no `simulateV1Url` (defensive —
7
+ * the type already requires it at construction).
9
8
  */
10
9
  export function resolveChain(config, chainId) {
11
10
  const entry = config.chains.get(chainId);
12
- if (!entry || (!entry.tenderlyRpc && !entry.simulateV1Url)) {
11
+ if (!entry?.simulateV1Url)
13
12
  throw new UnsupportedChainError(chainId);
14
- }
15
- return {
16
- tenderlyRpc: entry.tenderlyRpc,
17
- simulateV1Url: entry.simulateV1Url,
18
- };
13
+ return entry;
19
14
  }
@@ -0,0 +1,58 @@
1
+ import type { Address } from "viem";
2
+ import type { SimulationTransaction } from "../../types.js";
3
+ import type { ParsedRequest } from "../request/parse-request.js";
4
+ import type { ReadPhase, StateRead } from "../state/contract.js";
5
+ /** One call planned inside the single `eth_simulateV1` run. @internal */
6
+ export type PlannedCall = {
7
+ /** What the call carries on the wire. */
8
+ readonly transaction: SimulationTransaction & {
9
+ readonly value: bigint;
10
+ };
11
+ } & ({
12
+ /** A caller transaction. */
13
+ readonly type: "transaction";
14
+ readonly transactionIndex: number;
15
+ } | {
16
+ /** A preview-authorization preparation call. */
17
+ readonly type: "preparation";
18
+ readonly authorizationIndex: number;
19
+ readonly callIndex: number;
20
+ } | {
21
+ /** A state read-back call at the `before`/`after` phase. */
22
+ readonly type: "stateRead";
23
+ readonly phase: ReadPhase;
24
+ readonly read: StateRead;
25
+ });
26
+ /** The ordered call plan for one simulation. @internal */
27
+ export interface ExecutionPlan {
28
+ readonly request: ParsedRequest;
29
+ readonly calls: readonly PlannedCall[];
30
+ }
31
+ /**
32
+ * Plan the execution of a request as ordered `eth_simulateV1` calls.
33
+ *
34
+ * Sequence (design §plan order): every `before` state read → preparation
35
+ * calls → every user transaction → every `after` state read.
36
+ *
37
+ * Preparation calls run `from` the owner; state reads run `from`
38
+ * `zeroAddress` and never receive a public `txIdx`.
39
+ *
40
+ * @param request - The parsed request.
41
+ * @param owner - The bundle owner; sender of preparation calls.
42
+ * @param preparations - Ordered authorization preparations to simulate.
43
+ * @param reads - The full state-read list, replayed at `before` and `after`.
44
+ * @returns A deep-frozen {@link ExecutionPlan}; pure — equal inputs produce
45
+ * structurally equal plans.
46
+ * @internal
47
+ */
48
+ export declare function planExecution(params: {
49
+ readonly request: ParsedRequest;
50
+ readonly owner: Address;
51
+ readonly preparations: readonly {
52
+ readonly authorizationIndex: number;
53
+ readonly calls: readonly (SimulationTransaction & {
54
+ readonly value: bigint;
55
+ })[];
56
+ }[];
57
+ readonly reads: readonly StateRead[];
58
+ }): ExecutionPlan;
@@ -0,0 +1,72 @@
1
+ import { deepFreeze } from "@morpho-org/morpho-ts";
2
+ import { zeroAddress } from "viem";
3
+ /**
4
+ * Plan the execution of a request as ordered `eth_simulateV1` calls.
5
+ *
6
+ * Sequence (design §plan order): every `before` state read → preparation
7
+ * calls → every user transaction → every `after` state read.
8
+ *
9
+ * Preparation calls run `from` the owner; state reads run `from`
10
+ * `zeroAddress` and never receive a public `txIdx`.
11
+ *
12
+ * @param request - The parsed request.
13
+ * @param owner - The bundle owner; sender of preparation calls.
14
+ * @param preparations - Ordered authorization preparations to simulate.
15
+ * @param reads - The full state-read list, replayed at `before` and `after`.
16
+ * @returns A deep-frozen {@link ExecutionPlan}; pure — equal inputs produce
17
+ * structurally equal plans.
18
+ * @internal
19
+ */
20
+ export function planExecution(params) {
21
+ const { request, owner, preparations, reads } = params;
22
+ const calls = [];
23
+ for (const read of reads) {
24
+ calls.push({
25
+ type: "stateRead",
26
+ phase: "before",
27
+ read,
28
+ transaction: {
29
+ from: zeroAddress,
30
+ to: read.to,
31
+ data: read.data,
32
+ value: 0n,
33
+ },
34
+ });
35
+ }
36
+ for (const preparation of preparations) {
37
+ preparation.calls.forEach((transaction, callIndex) => {
38
+ calls.push({
39
+ type: "preparation",
40
+ authorizationIndex: preparation.authorizationIndex,
41
+ callIndex,
42
+ transaction: { ...transaction, from: owner },
43
+ });
44
+ });
45
+ }
46
+ request.transactions.forEach((transaction, i) => {
47
+ calls.push({
48
+ type: "transaction",
49
+ transactionIndex: i,
50
+ transaction: {
51
+ from: transaction.from,
52
+ to: transaction.to,
53
+ data: transaction.data,
54
+ value: transaction.value ?? 0n,
55
+ },
56
+ });
57
+ });
58
+ for (const read of reads) {
59
+ calls.push({
60
+ type: "stateRead",
61
+ phase: "after",
62
+ read,
63
+ transaction: {
64
+ from: zeroAddress,
65
+ to: read.to,
66
+ data: read.data,
67
+ value: 0n,
68
+ },
69
+ });
70
+ }
71
+ return deepFreeze({ request, calls });
72
+ }