Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 48 additions & 19 deletions src/utilsTenderly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -247,7 +263,9 @@ export async function simulateUserOperationWithTenderly(
// override so the simulation passes.
if (
!isV6UserOperation &&
(userOperation as UserOperationV7 | UserOperationV8 | UserOperationV9).factory === "0x7702"
isEip7702FactorySentinel(

Copy link
Copy Markdown
Member

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 factoryData is non-empty. The initCode construction above concatenates the data immediately after 0x7702, producing e.g. 0x77021234; EntryPoint requires 0x7702 right-padded to 20 bytes before the initialization data. A live Tenderly run decoded exactly 0x77021234 and reverted through the ordinary deployment path with AA10 sender already constructed. Please normalize the short marker during initCode construction and add a regression test for non-empty factoryData.

(userOperation as UserOperationV7 | UserOperationV8 | UserOperationV9).factory,
)
) {
const eip7702Auth = (userOperation as UserOperationV8 | UserOperationV9).eip7702Auth;
if (eip7702Auth != null && eip7702Auth.address != null) {
Expand Down Expand Up @@ -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;
Expand All @@ -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)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 factoryData as the sender initialization call. In live Tenderly simulation 001431d4-9e83-409d-b667-49cafe163d16, this path returned status=true while executing no internal transfer; adding the missing code override made the same call execute successfully (c727864a-f42e-4e0e-8228-371482f24fa2). Please mirror the full simulation's delegation override here and, when factoryData is non-empty, enqueue the SenderCreator-to-sender initialization before the call-data transaction.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

factory = null;
factoryData = null;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading