Skip to content
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

wip: eip-7702 #259

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
7 changes: 7 additions & 0 deletions packages/api/src/dto/EstimateUserOperation.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export class EstimateUserOperation {
@IsString()
@IsOptional()
paymasterData?: BytesLike;

/**
* EIP-7702 fields
*/
@IsEthereumAddress()
@IsOptional()
authorizationContract?: string;
}

export class EstimateUserOperationGasArgs {
Expand Down
11 changes: 11 additions & 0 deletions packages/api/src/dto/SendUserOperation.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ export class SendUserOperation {
@IsString()
@IsOptional()
paymasterData?: BytesLike;

/**
* EIP-7702 fields
*/
@IsEthereumAddress()
@IsOptional()
authorizationContract?: string;

@IsOptional()
@IsString()
authorizationSignature?: string;
}

export class SendUserOperationGasArgs {
Expand Down
3 changes: 2 additions & 1 deletion packages/executor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"async-mutex": "0.4.0",
"ethers": "5.7.2",
"strict-event-emitter-types": "2.0.0",
"ws": "8.16.0"
"ws": "8.16.0",
"viem": "2.21.37"
},
"devDependencies": {
"@types/ws": "8.2.2"
Expand Down
5 changes: 5 additions & 0 deletions packages/executor/src/entities/MempoolEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ export class MempoolEntry implements IMempoolEntry {
this.userOp.paymasterVerificationGasLimit,
paymasterPostOpGasLimit: this.userOp.paymasterPostOpGasLimit,
paymasterData: this.userOp.paymasterData,
authorizationContract: this.userOp.authorizationContract,
authorizationSignature: hexValue(
this.userOp.authorizationSignature || ""
),
authorizationNonce: this.userOp.authorizationNonce,
},
prefund: hexValue(this.prefund),
aggregator: this.aggregator,
Expand Down
3 changes: 3 additions & 0 deletions packages/executor/src/entities/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export interface MempoolEntrySerialized {
paymasterVerificationGasLimit?: BigNumberish;
paymasterPostOpGasLimit?: BigNumberish;
paymasterData?: BytesLike;
authorizationContract?: string;
authorizationSignature: BytesLike;
authorizationNonce?: number;
};
prefund: string;
aggregator: string | undefined;
Expand Down
42 changes: 40 additions & 2 deletions packages/executor/src/modules/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { GetNodeAPI, NetworkConfig } from "../interfaces";
import { EntryPointVersion } from "../services/EntryPointService/interfaces";
import { getUserOpGasLimit } from "../services/BundlingService/utils";
import { maxBn, minBn } from "../utils/bignumber";
import { validateAuthorization } from "../services/BundlingService/utils/eip7702";
import {
EstimateUserOperationGasArgs,
SendUserOperationGasArgs,
Expand Down Expand Up @@ -109,6 +110,31 @@ export class Eth {
entryPoint,
userOp
);

// eip-7702 validation
if (userOp.authorizationContract) {
if (
userOp.authorizationSignature == undefined ||
userOp.authorizationNonce == undefined
) {
throw new RpcError(
"Invalid Authorization. Missing fields",
RpcErrorCodes.INVALID_USEROP
);
}
const valid = await validateAuthorization(
this.chainId,
userOp,
userOp.authorizationNonce
);
if (!valid) {
throw new RpcError(
"Invalid authorization. Check signature",
RpcErrorCodes.INVALID_USEROP
);
}
}

await this.mempoolService.addUserOp(
userOp,
entryPoint,
Expand Down Expand Up @@ -225,9 +251,16 @@ export class Eth {
});
//>

const eip7702 =
userOp.authorizationContract != undefined &&
userOp.authorizationContract.length > 2;
let callGasLimit = minBn(binarySearchCGL, paidFeeCGL);
// check between binary search & paid fee cgl
if (userOp.factoryData !== undefined && userOp.factoryData.length <= 2) {
if (
userOp.factoryData !== undefined &&
userOp.factoryData.length <= 2 &&
!eip7702
) {
await this.provider
.estimateGas({
from: entryPoint,
Expand All @@ -241,7 +274,11 @@ export class Eth {
}

// check between eth_estimateGas & binary search & paid fee cgl
if (userOp.factoryData !== undefined && userOp.factoryData.length <= 2) {
if (
userOp.factoryData !== undefined &&
userOp.factoryData.length <= 2 &&
!eip7702
) {
const prevCGL = callGasLimit;
callGasLimit = minBn(ethEstimateGas, callGasLimit);
await this.provider
Expand Down Expand Up @@ -310,6 +347,7 @@ export class Eth {
callGasLimit,
maxFeePerGas: gasFee.maxFeePerGas,
maxPriorityFeePerGas: gasFee.maxPriorityFeePerGas,
delegate: eip7702 ? this.config.relayers[0] : undefined,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MempoolService } from "../../MempoolService";
import { ReputationService } from "../../ReputationService";
import { ExecutorEventBus } from "../../SubscriptionService";
import { EntryPointService } from "../../EntryPointService";
import { odysseyTestnet } from "viem/chains";

const WAIT_FOR_TX_MAX_RETRIES = 3; // 3 blocks

Expand Down Expand Up @@ -183,6 +184,7 @@ export abstract class BaseRelayer implements IRelayingMode {
transactionRequest: providers.TransactionRequest
): Promise<boolean> {
if (this.networkConfig.skipBundleValidation) return true;
if (this.chainId == odysseyTestnet.id) return true;
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { gasLimit: _, ...txWithoutGasLimit } = transactionRequest;
Expand Down
24 changes: 20 additions & 4 deletions packages/executor/src/services/BundlingService/relayers/classic.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { providers } from "ethers";
import { providers, Wallet } from "ethers";
import { chainsWithoutEIP1559 } from "@skandha/params/lib";
import { AccessList } from "ethers/lib/utils";
import { AuthorizationList } from "viem/experimental";
import { odysseyTestnet } from "viem/chains";
import { Relayer } from "../interfaces";
import { Bundle, StorageMap } from "../../../interfaces";
import { estimateBundleGasLimit } from "../utils";
import { getAuthorizationList, getRaw7702Transaction } from "../utils/eip7702";
import { BaseRelayer } from "./base";

export class ClassicRelayer extends BaseRelayer {
Expand Down Expand Up @@ -100,7 +103,12 @@ export class ClassicRelayer extends BaseRelayer {
.map((entry) => entry.userOpHash)
.join(", ")}`
);
await this.submitTransaction(relayer, transaction, storageMap)
await this.submitTransaction(
relayer,
transaction,
storageMap,
await getAuthorizationList(this.chainId, bundle)
)
.then(async (txHash: string) => {
this.logger.debug(`Bundle submitted: ${txHash}`);
this.logger.debug(
Expand Down Expand Up @@ -150,9 +158,17 @@ export class ClassicRelayer extends BaseRelayer {
private async submitTransaction(
relayer: Relayer,
transaction: providers.TransactionRequest,
storageMap: StorageMap
storageMap: StorageMap,
authorizationList?: AuthorizationList
): Promise<string> {
const signedRawTx = await relayer.signTransaction(transaction);
let signedRawTx = await relayer.signTransaction(transaction);
if (odysseyTestnet.id == this.chainId) {
signedRawTx = await getRaw7702Transaction(
transaction,
authorizationList!,
(relayer as Wallet).privateKey as `0x${string}`
);
}
const method = !this.networkConfig.conditionalTransactions
? "eth_sendRawTransaction"
: "eth_sendRawTransactionConditional";
Expand Down
123 changes: 123 additions & 0 deletions packages/executor/src/services/BundlingService/utils/eip7702.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {
Authorization,
AuthorizationList,
verifyAuthorization,
} from "viem/experimental";
import { BigNumber, ethers, providers } from "ethers";
import {
createPublicClient,
createWalletClient,
http,
parseSignature,
} from "viem";
import { odysseyTestnet } from "viem/chains";
import { UserOperation } from "@skandha/types/lib/contracts/UserOperation";
import { privateKeyToAccount } from "viem/accounts";
import { Bundle } from "../../../interfaces";

export async function validateAuthorization(
chainId: number,
userOp: UserOperation,
nonce: number
): Promise<boolean> {
const authorization: Authorization = {
contractAddress: userOp.authorizationContract as `0x${string}`,
chainId,
nonce: ethers.BigNumber.from(nonce).toNumber(),
...parseSignature(userOp.authorizationSignature as `0x${string}`),
};
console.log("authorization check", authorization);
return await verifyAuthorization({
address: userOp.sender as `0x${string}`,
authorization,
});
}

export async function getAuthorizationList(
chainId: number,
bundle: Bundle
): Promise<AuthorizationList> {
if (chainId != odysseyTestnet.id) return [];
const authorizationList = [];
for (const entry of bundle.entries) {
const { userOp } = entry;
if (!userOp.authorizationContract) continue;
const authorization: Authorization = {
contractAddress: userOp.authorizationContract as `0x${string}`,
chainId,
nonce: BigNumber.from(userOp.authorizationNonce!).toNumber(),
};
const { r, s, yParity } = parseSignature(
userOp.authorizationSignature as `0x${string}`
);
authorization.r = r;
authorization.s = s;
authorization.yParity = yParity;

authorizationList.push(authorization);
}
return authorizationList;
}

export async function getRaw7702Transaction(
transactionRequest: providers.TransactionRequest,
authorizationList: AuthorizationList,
relayer: `0x${string}`
): Promise<string> {
const wallet = createWalletClient({
transport: http(odysseyTestnet.rpcUrls.default.http[0]),
account: privateKeyToAccount(relayer),
});
const publicClient = createPublicClient({
chain: odysseyTestnet,
transport: http(odysseyTestnet.rpcUrls.default.http[0]),
});
const txRequest = {
chain: odysseyTestnet,
authorizationList,
to: transactionRequest.to as `0x${string}`,
gas:
transactionRequest.gasLimit != undefined
? BigNumber.from(transactionRequest.gasLimit).toBigInt()
: undefined,
maxFeePerGas:
transactionRequest.maxFeePerGas != undefined
? BigNumber.from(transactionRequest.maxFeePerGas).toBigInt()
: undefined,
maxPriorityFeePerGas:
transactionRequest.maxPriorityFeePerGas != undefined
? BigNumber.from(transactionRequest.maxPriorityFeePerGas).toBigInt()
: undefined,
data: transactionRequest.data as `0x${string}`,
nonce:
transactionRequest.nonce != undefined
? BigNumber.from(transactionRequest.nonce).toNumber()
: undefined,
};
// test
// txRequest.to = "0xd8da6bf26964af9d7eed9e03e53415d37aa96045";
// txRequest.data = "0x";
console.log("viem txrequest", txRequest);
// try {
// console.log(
// "estimation",
// await publicClient.estimateGas({
// ...txRequest,
// gas: undefined,
// })
// );
// } catch (err) {
// console.log(err);
// throw new Error("failed estimation");
// }
const transaction = await wallet
.signTransaction({
...txRequest,
type: "eip7702",
})
.catch((err) => {
console.log(err);
throw err;
});
return transaction;
}
13 changes: 12 additions & 1 deletion packages/executor/src/services/EntryPointService/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import RpcError from "@skandha/types/lib/api/errors/rpc-error";
import * as RpcErrorCodes from "@skandha/types/lib/api/errors/rpc-error-codes";
import { NetworkConfig, UserOpValidationResult } from "../../interfaces";
import { EntryPointV7Service, IEntryPointService } from "./versions";
import { EntryPointVersion } from "./interfaces";
import { EntryPointVersion, StateOverrides } from "./interfaces";

export class EntryPointService {
private entryPoints: {
Expand Down Expand Up @@ -194,4 +194,15 @@ export class EntryPointService {
getPaymaster(entryPoint: string, userOp: UserOperation): string | undefined {
return userOp.paymaster?.toLowerCase();
}

set7702StateOverrides(
entryPoint: string,
stateOverrides: StateOverrides,
userOp: UserOperation
): Promise<void> {
return this.entryPoints[entryPoint.toLowerCase()].set7702StateOverrides(
stateOverrides,
userOp
);
}
}
Loading
Loading