-
Notifications
You must be signed in to change notification settings - Fork 16
fix: Tenderly simulation initCode split, 7702 sentinel forms, and bigint precision #223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,22 @@ import type {Authorization7702Hex} from "./utils7702"; | |
| */ | ||
| export type OverrideType = Record<string, Record<string, string | Record<string, string>>>; | ||
|
|
||
| /** | ||
| * EIP-7702 userOps mark the factory field with a sentinel instead of a real | ||
| * factory: either the short form "0x7702" or the 20-byte right-padded form | ||
| * accepted by EntryPoint v0.8 (initCode starting with bytes 0x7702). | ||
| */ | ||
| function isEip7702FactorySentinel(factory: string | null | undefined): boolean { | ||
| if (factory == null) { | ||
| return false; | ||
| } | ||
| const factoryLowerCase = factory.toLowerCase(); | ||
| return ( | ||
| factoryLowerCase === "0x7702" || | ||
| factoryLowerCase === "0x7702000000000000000000000000000000000000" | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Shares an existing Tenderly simulation so it can be viewed via a public link. | ||
| * @param tenderlyAccountSlug - The Tenderly account slug. | ||
|
|
@@ -247,7 +263,9 @@ export async function simulateUserOperationWithTenderly( | |
| // override so the simulation passes. | ||
| if ( | ||
| !isV6UserOperation && | ||
| (userOperation as UserOperationV7 | UserOperationV8 | UserOperationV9).factory === "0x7702" | ||
| isEip7702FactorySentinel( | ||
| (userOperation as UserOperationV7 | UserOperationV8 | UserOperationV9).factory, | ||
| ) | ||
| ) { | ||
| const eip7702Auth = (userOperation as UserOperationV8 | UserOperationV9).eip7702Auth; | ||
| if (eip7702Auth != null && eip7702Auth.address != null) { | ||
|
|
@@ -478,8 +496,9 @@ export async function simulateUserOperationCallDataWithTenderly( | |
| let callData = userOperation.callData; | ||
| if ("initCode" in userOperation) { | ||
| if (userOperation.initCode != null && userOperation.initCode.length > 2) { | ||
| factory = userOperation.initCode.slice(0, 22); | ||
| factoryData = userOperation.initCode.slice(22); | ||
| // initCode = 20-byte factory address ("0x" + 40 hex chars) ‖ factoryData | ||
| factory = userOperation.initCode.slice(0, 42); | ||
| factoryData = `0x${userOperation.initCode.slice(42)}`; | ||
| } | ||
| } else { | ||
| factory = userOperation.factory; | ||
|
|
@@ -488,7 +507,7 @@ export async function simulateUserOperationCallDataWithTenderly( | |
| // EIP-7702 userOps use factory:"0x7702" as a sentinel with | ||
| // factoryData:null. This doesn't represent an actual factory | ||
| // deployment, so normalize to null. | ||
| if (factory === "0x7702") { | ||
| if (isEip7702FactorySentinel(factory)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This avoids treating the padded marker as a factory, but the direct simulation is still semantically incomplete: it neither installs the sender's EIP-7702 delegation-code override nor preserves non-empty
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
| factory = null; | ||
| factoryData = null; | ||
| } | ||
|
|
@@ -763,8 +782,8 @@ export async function callTenderlySimulateBundle( | |
| to: string; | ||
| data: string; | ||
| gas?: number | null; | ||
| gasPrice?: number | null; | ||
| value?: number | null; | ||
| gasPrice?: bigint | number | null; | ||
| value?: bigint | number | null; | ||
| blockNumber?: number | null; | ||
| simulationType?: "full" | "quick" | "abi"; | ||
| stateOverrides?: OverrideType | null; | ||
|
|
@@ -803,27 +822,37 @@ export async function callTenderlySimulateBundle( | |
| transactionObject.gas = transaction.gas; | ||
| } | ||
| if (transaction.gasPrice != null) { | ||
| transactionObject.gas_price = transaction.gasPrice; | ||
| // serialize bigint as a decimal string so wei amounts above 2^53 | ||
| // don't lose precision | ||
| transactionObject.gas_price = | ||
| typeof transaction.gasPrice === "bigint" | ||
| ? transaction.gasPrice.toString() | ||
| : transaction.gasPrice; | ||
| } | ||
| if (transaction.value != null) { | ||
| transactionObject.value = transaction.value; | ||
| transactionObject.value = | ||
| typeof transaction.value === "bigint" | ||
| ? transaction.value.toString() | ||
| : transaction.value; | ||
| } | ||
| if (transaction.stateOverrides != null) { | ||
| const stateOverrides = transaction.stateOverrides; | ||
| for (const address in stateOverrides) { | ||
| for (const key in stateOverrides[address]) { | ||
| // build a copy instead of rewriting the caller-owned object in place | ||
| const stateOverrides: OverrideType = {}; | ||
| for (const address in transaction.stateOverrides) { | ||
| const entry = { ...transaction.stateOverrides[address] }; | ||
| for (const key in entry) { | ||
| if (key !== "balance" && key !== "code" && key !== "storage" && key !== "stateDiff") { | ||
| throw new RangeError(`Invalid stateOverrides key: ${key}.`); | ||
| } else if ( | ||
| "storage" in stateOverrides[address] && | ||
| "stateDiff" in stateOverrides[address] | ||
| ) { | ||
| throw new RangeError("can't set both storage and stateDiff for stateOverrides"); | ||
| } else if ("stateDiff" in stateOverrides[address]) { | ||
| stateOverrides[address].storage = stateOverrides[address].stateDiff; | ||
| delete stateOverrides[address].stateDiff; | ||
| } | ||
| } | ||
| if ("storage" in entry && "stateDiff" in entry) { | ||
| throw new RangeError("can't set both storage and stateDiff for stateOverrides"); | ||
| } | ||
| if ("stateDiff" in entry) { | ||
| entry.storage = entry.stateDiff; | ||
| delete entry.stateDiff; | ||
| } | ||
| stateOverrides[address] = entry; | ||
| } | ||
| transactionObject.state_objects = stateOverrides; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expanded padded-marker detection is valid, but the short-marker case is still malformed when
factoryDatais non-empty. TheinitCodeconstruction above concatenates the data immediately after0x7702, producing e.g.0x77021234; EntryPoint requires0x7702right-padded to 20 bytes before the initialization data. A live Tenderly run decoded exactly0x77021234and reverted through the ordinary deployment path withAA10 sender already constructed. Please normalize the short marker duringinitCodeconstruction and add a regression test for non-emptyfactoryData.