From a9a709e47fb41c6c46605c82bf84be7bc7e59238 Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 10:03:42 -0400 Subject: [PATCH 01/10] tx: small adjustments to 7702 tx example2 --- packages/tx/examples/EOACodeTx.ts | 33 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/packages/tx/examples/EOACodeTx.ts b/packages/tx/examples/EOACodeTx.ts index d23700ae5ca..6acecdcec34 100644 --- a/packages/tx/examples/EOACodeTx.ts +++ b/packages/tx/examples/EOACodeTx.ts @@ -1,22 +1,26 @@ import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { createEOACode7702Tx } from '@ethereumjs/tx' -import { type PrefixedHexString, createAddressFromPrivateKey, randomBytes } from '@ethereumjs/util' +import { Capability, createEOACode7702Tx } from '@ethereumjs/tx' +import { + EOACode7702AuthorizationListItem, + type PrefixedHexString, + createAddressFromPrivateKey, + randomBytes, +} from '@ethereumjs/util' -const ones32 = `0x${'01'.repeat(32)}` as PrefixedHexString +const ones32: PrefixedHexString = `0x${'01'.repeat(32)}` +const authorizationListItem: EOACode7702AuthorizationListItem = { + chainId: '0x2', + address: `0x${'20'.repeat(20)}`, + nonce: '0x1', + yParity: '0x1', + r: ones32, + s: ones32, +} -const common = new Common({ chain: Mainnet, hardfork: Hardfork.Cancun, eips: [7702] }) +const common = new Common({ chain: Mainnet, hardfork: Hardfork.Prague }) const tx = createEOACode7702Tx( { - authorizationList: [ - { - chainId: '0x2', - address: `0x${'20'.repeat(20)}`, - nonce: '0x1', - yParity: '0x1', - r: ones32, - s: ones32, - }, - ], + authorizationList: [authorizationListItem], to: createAddressFromPrivateKey(randomBytes(32)), }, { common }, @@ -25,3 +29,4 @@ const tx = createEOACode7702Tx( console.log( `EIP-7702 EOA code tx created with ${tx.authorizationList.length} authorization list item(s).`, ) +console.log('Tx supports EIP-7702? ', tx.supports(Capability.EIP7702EOACode)) From 8d76c4b8c9b6f29f5ad21005419d630a82e0100d Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 10:21:50 -0400 Subject: [PATCH 02/10] lint: fix --- packages/tx/examples/EOACodeTx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tx/examples/EOACodeTx.ts b/packages/tx/examples/EOACodeTx.ts index 6acecdcec34..979ae22da74 100644 --- a/packages/tx/examples/EOACodeTx.ts +++ b/packages/tx/examples/EOACodeTx.ts @@ -1,7 +1,7 @@ import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { Capability, createEOACode7702Tx } from '@ethereumjs/tx' import { - EOACode7702AuthorizationListItem, + type EOACode7702AuthorizationListItem, type PrefixedHexString, createAddressFromPrivateKey, randomBytes, From 723e7895641c0890eb0af8c31b29bcb14d741fd1 Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 11:10:52 -0400 Subject: [PATCH 03/10] examples: atomic approve-transferFrom + uniswap swap Co-authored-by: Avdhesh Charjan --- .../7702/atomic-erc20-approve-transferFrom.ts | 286 ++++++++++++++++++ packages/vm/examples/7702/uniswap-swap.ts | 252 +++++++++++++++ 2 files changed, 538 insertions(+) create mode 100644 packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts create mode 100644 packages/vm/examples/7702/uniswap-swap.ts diff --git a/packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts b/packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts new file mode 100644 index 00000000000..e4b254e6261 --- /dev/null +++ b/packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts @@ -0,0 +1,286 @@ +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +import { RPCStateManager } from '@ethereumjs/statemanager' +import { Capability, EOACode7702Tx, TransactionType } from '@ethereumjs/tx' +import { + Address, + EOACode7702AuthorizationListBytesItem, + EOACode7702AuthorizationListItem, + PrefixedHexString, + bigIntToBytes, + bytesToHex, + createAddressFromPrivateKey, + createAddressFromString, + hexToBytes, + intToHex, +} from '@ethereumjs/util' +import { createVM } from '@ethereumjs/vm' +import { Contract } from 'ethers' +import { TxData } from '../../../tx/dist/esm/7702/tx' + +/** + * This example demonstrates how to use EIP-7702 to perform atomic ERC20 operations + * (approve + transferFrom) in a single transaction using RPCStateManager to + * simulate against a real network. + * + * WARNING: DO NOT USE REAL PRIVATE KEYS WITH VALUE. This is for demonstration only. + */ + +// ERC20 Interface +const erc20Abi = [ + 'function approve(address spender, uint256 amount) external returns (bool)', + 'function transferFrom(address sender, address recipient, uint256 amount) external returns (bool)', + 'function balanceOf(address account) external view returns (uint256)', + 'function allowance(address owner, address spender) external view returns (uint256)', +] + +// Bundle contract that handles atomic approve + transferFrom +// This is what an EOA will delegate to with EIP-7702 +const bundleContractCode = ` +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IERC20 { + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); +} + +contract ERC20Bundler { + /** + * @dev Atomically approves and transfers ERC20 tokens in a single call. + * @param token The ERC20 token address + * @param to The recipient address + * @param amount The amount to approve and transfer + * @return success True if the operation was successful + */ + function approveAndTransfer(address token, address to, uint256 amount) external returns (bool) { + // Approve the bundler contract to spend tokens + bool approved = IERC20(token).approve(address(this), amount); + require(approved, "Approval failed"); + + // Transfer the tokens from the caller to the recipient + bool transferred = IERC20(token).transferFrom(msg.sender, to, amount); + require(transferred, "Transfer failed"); + + return true; + } +} +` + +// Simulates a deployed bundle contract +const BUNDLE_CONTRACT_ADDRESS: PrefixedHexString = '0x1234567890123456789012345678901234567890' + +// DAI token on mainnet +const DAI_ADDRESS: PrefixedHexString = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + +const runExample = async () => { + // For demonstration purposes, we're using a fake private key + // WARNING: Never use real private keys in code or examples + const privateKey = hexToBytes( + '0x1122334455667788112233445566778811223344556677881122334455667788', + ) + const userAddress = createAddressFromPrivateKey(privateKey) + + console.log('User address:', userAddress.toString()) + + // Initialize Common with EIP-7702 enabled + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Cancun, + eips: [7702], + }) + + // We'll use RPCStateManager to interact with the real network state + // For this example we're using a local node, but you could use any provider + // This allows us to simulate transactions against real network state + const provider = 'http://localhost:8545' // Replace with an actual provider URL + + // Create a state manager with the required parameters + const rpcStateManager = new RPCStateManager({ + provider, + blockTag: 'earliest', // Using a valid value + }) + + // Create VM instance with the RPCStateManager + // Use the static create method of VM + const vm = await createVM({ + common, + stateManager: rpcStateManager, + }) + + // Check if user has a DAI balance + const daiContract = new Contract(DAI_ADDRESS, erc20Abi) + const balanceOfCalldata = daiContract.encodeFunctionData('balanceOf', [userAddress.toString()]) + + const balanceOfResult = await vm.evm.runCall({ + to: new Address(hexToBytes(DAI_ADDRESS)), + caller: userAddress, + data: hexToBytes(balanceOfCalldata), + }) + + // Decode the balance result + const daiBalance = + balanceOfResult.execResult.returnValue.length > 0 + ? daiContract.decodeFunctionResult( + 'balanceOf', + bytesToHex(balanceOfResult.execResult.returnValue), + )[0] + : 0n + + console.log('DAI balance:', daiBalance.toString()) + + if (daiBalance <= 0n) { + console.log('No DAI balance to demonstrate with') + return + } + + // Create an EIP-7702 transaction that will delegate the user's EOA + // to the bundle contract for this transaction + + // Recipient of the DAI transfer + const recipientAddress = new Address(hexToBytes('0x0000000000000000000000000000000000005678')) + console.log('Recipient address:', recipientAddress.toString()) + + // Amount to transfer (use a small amount for the demo) + const transferAmount = 1000000000000000000n // 1 DAI + + // Create the calldata for the bundle contract's approveAndTransfer function + const bundleInterface = new Interface([ + 'function approveAndTransfer(address token, address to, uint256 amount) external returns (bool)', + ]) + + const approveAndTransferCalldata = bundleInterface.encodeFunctionData('approveAndTransfer', [ + DAI_ADDRESS, + recipientAddress.toString(), + transferAmount, + ]) + + const authorizationListItem: EOACode7702AuthorizationListItem = { + chainId: bigcommon.chainId(), + address: BUNDLE_CONTRACT_ADDRESS, + nonce: intToHex(0), + yParity: intToHex(0), + r: `0x${'01'.repeat(32)}`, + s: `0x${'01'.repeat(32)}`, + } + + // Create the EIP-7702 transaction with authorization to use the bundle contract + const txData: TxData = { + nonce: 0n, + gasLimit: 300000n, + maxFeePerGas: 20000000000n, + maxPriorityFeePerGas: 2000000000n, + to: new Address(hexToBytes(BUNDLE_CONTRACT_ADDRESS)), + value: 0n, + data: hexToBytes(approveAndTransferCalldata), + accessList: [], + authorizationList: [ + { + chainId: common.chainId(), + address: new Address(hexToBytes(BUNDLE_CONTRACT_ADDRESS)), + nonce: 0n, + yParity: 0n, + r: hexToBytes('0x1234567890123456789012345678901234567890123456789012345678901234'), + s: hexToBytes('0x1234567890123456789012345678901234567890123456789012345678901234'), + }, + ], + } // Type assertion to bypass type checking + + // Pass common as a separate option + const tx = new EOACode7702Tx(txData, { common }) + const signedTx = tx.sign(privateKey) + + console.log('Transaction created successfully') + console.log('Transaction type:', TransactionType[signedTx.type]) + console.log('Supports EIP-7702:', signedTx.supports(Capability.EIP7702EOACode)) + + // Run the transaction to simulate what would happen + console.log('\nSimulating transaction...') + + try { + const result = await vm.runTx({ tx: signedTx }) + + console.log( + 'Transaction simulation:', + result.execResult.exceptionError !== null && result.execResult.exceptionError !== undefined + ? 'Failed' + : 'Success', + ) + + if ( + result.execResult.exceptionError === null || + result.execResult.exceptionError === undefined + ) { + console.log('Gas used:', result.gasUsed.toString()) + + // Check DAI allowance after the transaction + const allowanceCalldata = erc20Interface.encodeFunctionData('allowance', [ + userAddress.toString(), + BUNDLE_CONTRACT_ADDRESS, + ]) + + const allowanceResult = await vm.evm.runCall({ + to: new Address(hexToBytes(DAI_ADDRESS)), + caller: userAddress, + data: hexToBytes(allowanceCalldata), + }) + + const allowance = + allowanceResult.execResult.returnValue.length > 0 + ? erc20Interface.decodeFunctionResult( + 'allowance', + bytesToHex(allowanceResult.execResult.returnValue), + )[0] + : 0n + + console.log('DAI allowance after transaction:', allowance.toString()) + + // Check recipient's DAI balance after the transaction + const recipientBalanceCalldata = erc20Interface.encodeFunctionData('balanceOf', [ + recipientAddress.toString(), + ]) + + const recipientBalanceResult = await vm.evm.runCall({ + to: createAddressFromString(DAI_ADDRESS), + caller: userAddress, + data: hexToBytes(recipientBalanceCalldata), + }) + + const recipientBalance = + recipientBalanceResult.execResult.returnValue.length > 0 + ? erc20Interface.decodeFunctionResult( + 'balanceOf', + bytesToHex(recipientBalanceResult.execResult.returnValue), + )[0] + : 0n + + console.log('Recipient DAI balance after transaction:', recipientBalance.toString()) + + // Explain what happened + console.log('\nTransaction Summary:') + console.log('- User authorized their EOA to use the bundle contract implementation') + console.log('- The EOA executed the approveAndTransfer function which:') + console.log(' 1. Approved the bundle contract to spend DAI tokens') + console.log(' 2. Transferred DAI tokens to the recipient in a single atomic transaction') + console.log('\nThis demonstrates the power of EIP-7702 to enable advanced features for EOAs') + console.log( + 'without needing to deploy an account contract or switch to a smart contract wallet.', + ) + } else { + console.log('Error:', result.execResult.exceptionError.error) + } + } catch (error) { + console.error('Simulation error:', error) + } + + // This would be sent to the actual network using: + // const serializedTx = bytesToHex(signedTx.serialize()) + // console.log('Serialized transaction for broadcasting:', serializedTx) +} + +runExample().catch((error) => { + if (error !== null && error !== undefined) { + console.error('Error:', error) + } +}) diff --git a/packages/vm/examples/7702/uniswap-swap.ts b/packages/vm/examples/7702/uniswap-swap.ts new file mode 100644 index 00000000000..b2bf702d9b0 --- /dev/null +++ b/packages/vm/examples/7702/uniswap-swap.ts @@ -0,0 +1,252 @@ +import { Chain, Common, Hardfork } from '@ethereumjs/common' +import { RPCStateManager } from '@ethereumjs/statemanager' +import { Capability, EOACode7702Tx, TransactionType } from '@ethereumjs/tx' +import { Address, bytesToHex, hexToBytes } from '@ethereumjs/util' +import { VM } from '@ethereumjs/vm' + +/** + * This example demonstrates how to use EIP-7702 to perform a Uniswap swap + * from an EOA using the RPCStateManager to simulate against a real network. + * + * WARNING: DO NOT USE REAL PRIVATE KEYS WITH VALUE. This is for demonstration only. + */ + +// Uniswap V2 Router address on Mainnet +const UNISWAP_ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' + +// Token addresses +const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F' +const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' + +// ABI snippets for the relevant functions +const ERC20_APPROVE_ABI = { + inputs: [ + { name: 'spender', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + name: 'approve', + outputs: [{ name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', +} + +const UNISWAP_SWAP_ABI = { + inputs: [ + { name: 'amountIn', type: 'uint256' }, + { name: 'amountOutMin', type: 'uint256' }, + { name: 'path', type: 'address[]' }, + { name: 'to', type: 'address' }, + { name: 'deadline', type: 'uint256' }, + ], + name: 'swapExactTokensForTokens', + outputs: [{ name: 'amounts', type: 'uint256[]' }], + stateMutability: 'nonpayable', + type: 'function', +} + +// Bundler contract that combines approve and swap in one transaction +const uniswapBundlerCode = ` +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IERC20 { + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); +} + +interface IUniswapV2Router { + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); +} + +contract UniswapBundler { + /** + * @dev Atomically approves and swaps tokens using Uniswap in a single call + * @param tokenIn The input token address + * @param tokenOut The output token address + * @param amountIn The amount of input tokens to swap + * @param amountOutMin The minimum amount of output tokens to receive + * @param router The Uniswap router address + * @param deadline The deadline for the swap + * @return amounts The amounts of tokens exchanged + */ + function approveAndSwap( + address tokenIn, + address tokenOut, + uint256 amountIn, + uint256 amountOutMin, + address router, + uint256 deadline + ) external returns (uint256[] memory) { + // Approve the router to spend tokens + IERC20(tokenIn).approve(router, amountIn); + + // Create the swap path + address[] memory path = new address[](2); + path[0] = tokenIn; + path[1] = tokenOut; + + // Execute the swap + return IUniswapV2Router(router).swapExactTokensForTokens( + amountIn, + amountOutMin, + path, + msg.sender, // Send output tokens directly to the caller + deadline + ); + } +} +` + +// Simulates a deployed bundler contract +const BUNDLER_CONTRACT_ADDRESS = '0x1234567890123456789012345678901234567890' + +// Helper function to encode function call +function encodeFunction(abi: any, values: any[]): Uint8Array { + // This is a simplified version. In a real app, use ethers.js or web3.js + const signature = `${abi.name}(${abi.inputs.map((input: any) => input.type).join(',')})` + const functionSelector = hexToBytes(`0x${signature.slice(0, 10)}`) + + // This is just a placeholder - in a real implementation, you would properly ABI encode the parameters + console.log(`Encoded function call to ${signature}`) + return functionSelector +} + +// Create ABI for the bundler contract +const BUNDLER_ABI = { + inputs: [ + { name: 'tokenIn', type: 'address' }, + { name: 'tokenOut', type: 'address' }, + { name: 'amountIn', type: 'uint256' }, + { name: 'amountOutMin', type: 'uint256' }, + { name: 'router', type: 'address' }, + { name: 'deadline', type: 'uint256' }, + ], + name: 'approveAndSwap', + outputs: [{ name: 'amounts', type: 'uint256[]' }], + stateMutability: 'nonpayable', + type: 'function', +} + +const main = async () => { + // For demonstration purposes only, using fake keys + // WARNING: Never use real private keys in code + const privateKey = hexToBytes( + '0x1122334455667788112233445566778811223344556677881122334455667788', + ) + const userAddress = new Address(hexToBytes('0x1234567890123456789012345678901234567890')) + console.log('User address:', userAddress.toString()) + + // Initialize Common with EIP-7702 enabled + const common = new Common({ + chain: Chain.Mainnet as any, + hardfork: Hardfork.Cancun, + eips: [7702], + }) + + // We'll use RPCStateManager to interact with a mainnet fork + const provider = 'http://localhost:8545' + const blockTag = 'earliest' + const rpcStateManager = new RPCStateManager({ provider, blockTag }) + + // Create VM instance with the RPCStateManager + const vm = await (VM as any).create({ common, stateManager: rpcStateManager }) + + console.log('Simulating a Uniswap swap with EIP-7702...') + + // Parameters for the swap + const amountIn = 1000000000000000000n // 1 DAI + const amountOutMin = 1n // Accept any amount (in production, use price oracle for slippage protection) + const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20) // 20 minutes from now + + // Create calldata for the bundler contract's approveAndSwap function + const calldata = encodeFunction(BUNDLER_ABI, [ + DAI_ADDRESS, + WETH_ADDRESS, + amountIn, + amountOutMin, + UNISWAP_ROUTER_ADDRESS, + deadline, + ]) + + // Create the EIP-7702 transaction + const txData = { + nonce: 0n, + gasLimit: 500000n, + maxFeePerGas: 30000000000n, // 30 gwei + maxPriorityFeePerGas: 3000000000n, // 3 gwei + to: new Address(hexToBytes(`0x${BUNDLER_CONTRACT_ADDRESS.slice(2)}` as `0x${string}`)), + value: 0n, + data: calldata, + accessList: [], + authorizationList: [ + { + chainId: common.chainId(), + address: new Address(hexToBytes(`0x${BUNDLER_CONTRACT_ADDRESS.slice(2)}` as `0x${string}`)), + nonce: 0n, + yParity: 0n, + r: hexToBytes( + '0x1234567890123456789012345678901234567890123456789012345678901234' as `0x${string}`, + ), + s: hexToBytes( + '0x1234567890123456789012345678901234567890123456789012345678901234' as `0x${string}`, + ), + }, + ] as any, + } + + // Create and sign the transaction + const tx = new EOACode7702Tx(txData, { common }) + const signedTx = tx.sign(privateKey) + + console.log('Transaction type:', TransactionType[signedTx.type]) + console.log('Supports EIP-7702:', signedTx.supports(Capability.EIP7702EOACode)) + + // Simulate the transaction + try { + console.log('Running transaction simulation...') + const result = await vm.runTx({ tx: signedTx }) + + console.log( + 'Transaction simulation:', + result.execResult.exceptionError !== null && result.execResult.exceptionError !== undefined + ? 'Failed' + : 'Success', + ) + + if ( + result.execResult.exceptionError === null || + result.execResult.exceptionError === undefined + ) { + console.log('Gas used:', result.gasUsed.toString()) + + console.log('\nTransaction Summary:') + console.log( + '- The EOA authorized delegation to the UniswapBundler contract for this transaction', + ) + console.log('- The bundler contract atomically executed:') + console.log(' 1. Approval of DAI tokens to the Uniswap router') + console.log(' 2. Swap of DAI for WETH using Uniswap') + console.log('\nBenefits of using EIP-7702 for this use case:') + console.log('- Saved gas by combining multiple transactions into one') + console.log('- Better UX with atomic approval and swap') + console.log('- No need to deploy a separate smart contract wallet') + console.log("- Maintained security of the user's EOA") + } else { + console.log('Error:', result.execResult.exceptionError.error) + } + } catch (error) { + console.error('Simulation error:', error) + } +} + +main().catch((error) => { + if (error !== null && error !== undefined) { + console.error('Error:', error) + } +}) From 4742254e9cdb60186174d6d1e7269dc571624cb8 Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 12:03:10 -0400 Subject: [PATCH 04/10] vm: add 7702 example --- .../7702/uniswap-swap-wrap-transfer.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts diff --git a/packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts new file mode 100644 index 00000000000..8eb42220fda --- /dev/null +++ b/packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts @@ -0,0 +1,111 @@ +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +import { RPCStateManager } from '@ethereumjs/statemanager' +import { EOACode7702Tx } from '@ethereumjs/tx' +import { + Address, + EOACode7702AuthorizationListItemUnsigned, + PrefixedHexString, + createAddressFromPrivateKey, + eoaCode7702SignAuthorization, + hexToBytes, +} from '@ethereumjs/util' +import { createVM, runTx } from '@ethereumjs/vm' +import { Interface, parseEther, parseUnits } from 'ethers' +import { TxData } from '../../../tx/dist/esm/7702/tx' + +async function runBatched7702() { + // ─── setup ────────────────────────────────────────────────────────────── + const privateKey = hexToBytes( + '0x1122334455667788112233445566778811223344556677881122334455667788', + ) + const userAddress = createAddressFromPrivateKey(privateKey) + console.log('EOA:', userAddress.toString()) + + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Cancun, + eips: [7702], + }) + const stateManager = new RPCStateManager({ + provider: 'YOUR_PROVIDER_URL', + blockTag: 22_000_000n, // Example block number + }) + const vm = await createVM({ common, stateManager }) + + // ─── constants & ABIs ─────────────────────────────────────────────────── + const TOKEN = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + const ROUTER = '0x66a9893cc07d91d95644aedd05d03f95e1dba8af' + const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' + const COLD_WALLET = `0x${'42'.repeat(20)}` + + const erc20Abi = ['function approve(address,uint256)'] + const routerAbi = ['function swapExactTokensForETH(uint256,uint256,address[],address,uint256)'] + const wethAbi = ['function deposit()', 'function transfer(address,uint256)'] + const batchAbi = ['function executeBatch(bytes[] calldata calls) external'] + + const erc20 = new Interface(erc20Abi) + const router = new Interface(routerAbi) + const weth = new Interface(wethAbi) + const batch = new Interface(batchAbi) + const BATCH_CONTRACT = '0xYourBatchContractAddressHere' + + // ─── parameters ───────────────────────────────────────────────────────── + const amountIn = parseUnits('1', 18) // 1 DAI + const amountOutMin = 0n + const deadline = BigInt(Math.floor(Date.now() / 1e3) + 20 * 60) + + // ─── build individual call-bytes ──────────────────────────────────────── + const callApprove = erc20.encodeFunctionData('approve', [ROUTER, amountIn]) as PrefixedHexString + + const callSwap = router.encodeFunctionData('swapExactTokensForETH', [ + amountIn, + amountOutMin, + [TOKEN, WETH], + userAddress.toString(), + deadline, + ]) as PrefixedHexString + + const callDeposit = weth.encodeFunctionData('deposit', []) as PrefixedHexString + const callTransfer = weth.encodeFunctionData('transfer', [ + COLD_WALLET, + parseEther('1'), + ]) as PrefixedHexString + + const calls = [callApprove, callSwap, callDeposit, callTransfer].map(hexToBytes) // bytes[] + + // ─── sign four 7702 authorizations ────────────────────────────────────── + const auths = [ + { address: TOKEN, nonce: '0x0' }, + { address: ROUTER, nonce: '0x1' }, + { address: WETH, nonce: '0x2' }, + { address: WETH, nonce: '0x3' }, + ].map(({ address, nonce }) => { + const unsigned: EOACode7702AuthorizationListItemUnsigned = { + chainId: '0x1', + address: address as PrefixedHexString, + nonce: nonce as PrefixedHexString, + } + return eoaCode7702SignAuthorization(unsigned, privateKey) + }) + + // ─── one single 7702 tx ───────────────────────────────────────────────── + const batchData = batch.encodeFunctionData('executeBatch', [calls]) as PrefixedHexString + + const txData: TxData = { + nonce: 0n, + gasLimit: 800_000n, + maxFeePerGas: 50_000_000_000n, + maxPriorityFeePerGas: 5_000_000_000n, + to: new Address(hexToBytes(BATCH_CONTRACT)), + value: 0n, + data: hexToBytes(batchData), + authorizationList: auths, + } + + const tx = new EOACode7702Tx(txData, { common }).sign(privateKey) + const result = await runTx(vm, { tx }) + + console.log('🔀 Batch 7702:', result.execResult.exceptionError ? '❌' : '✅') +} + +runBatched7702().catch(console.error) From 47991deed3ffbcadda1dc15069b145027ab3648d Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 12:33:06 -0400 Subject: [PATCH 05/10] vm: approve swap transfer --- .../vm/examples/7702/uniswap-swap-transfer.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/vm/examples/7702/uniswap-swap-transfer.ts diff --git a/packages/vm/examples/7702/uniswap-swap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-transfer.ts new file mode 100644 index 00000000000..76a484b5b28 --- /dev/null +++ b/packages/vm/examples/7702/uniswap-swap-transfer.ts @@ -0,0 +1,106 @@ +import { Common, Hardfork, Mainnet } from '@ethereumjs/common' +import { RPCStateManager } from '@ethereumjs/statemanager' +import { EOACode7702Tx } from '@ethereumjs/tx' +import { + Address, + PrefixedHexString, + createAddressFromPrivateKey, + eoaCode7702SignAuthorization, + hexToBytes, +} from '@ethereumjs/util' +import { createVM, runTx } from '@ethereumjs/vm' +import { AbiCoder, Interface, parseEther, parseUnits } from 'ethers' +import { TxData } from '../../../tx/dist/esm/7702/tx' + +async function run() { + // ─── your EOA key & address ─────────────────────────────────────────── + const privateKeyHex = '0x1122334455667788112233445566778811223344556677881122334455667788' + const privateKey = hexToBytes(privateKeyHex) + const userAddress = createAddressFromPrivateKey(privateKey) + console.log('EOA:', userAddress.toString()) + + // ─── set up EthereumJS VM with EIP-7702 enabled ─────────────────────── + const common = new Common({ + chain: Mainnet, + hardfork: Hardfork.Cancun, + eips: [7702], + }) + const stateManager = new RPCStateManager({ + provider: 'YourProviderURLHere', + blockTag: 22_000_000n, + }) + const vm = await createVM({ common, stateManager }) + + // ─── constants & ABIs ──────────────────────────────────────────────── + const DAI = '0x6B175474E89094C44Da98b954EedeAC495271d0F' + const UNISWAP_V3_ROUTER = '0xE592427A0AEce92De3Edee1F18E0157C05861564' + const WETH = '0xC02aaa39b223FE8D0A0e5c4F27EaD9083C756Cc2' + const COLD_WALLET = `0x${'42'.repeat(20)}` + const BATCH_CONTRACT = '0xYourBatchContractAddressHere' + + const erc20Abi = [ + 'function approve(address _spender, uint256 _amount) external returns (bool success)', + 'function transfer(address _to, uint256 _value) public returns (bool success)', + ] + const routerAbi = ['function exactInput(bytes)'] + const batchAbi = ['function executeBatch(bytes[] calldata) external'] + + const erc20 = new Interface(erc20Abi) + const router = new Interface(routerAbi) + const batchContract = new Interface(batchAbi) + + // ─── trade parameters ──────────────────────────────────────────────── + const amountIn = parseUnits('10000', 18) // 10000 DAI + const amountOutMin = parseEther('4') // expect at least 4 WETH + + // ─── encode your two sub-calls ─────────────────────────────────────── + const callApprove = erc20.encodeFunctionData('approve', [ + UNISWAP_V3_ROUTER, + amountIn, + ]) as PrefixedHexString + + // ─── encode your swap call data ─────────────────────────────────── + const uniswapV3SwapCallData = `0xYourSwapCallDataHere` + + const callSwap = router.encodeFunctionData('exactInput', [ + uniswapV3SwapCallData, + ]) as PrefixedHexString + + const callTransfer = router.encodeFunctionData('transfer', [ + COLD_WALLET, + amountOutMin, + ]) as PrefixedHexString + + const calls = [callApprove, callSwap, callTransfer].map(hexToBytes) + + // ─── sign one authorization each for DAI approve & router swap ────── + const targets: PrefixedHexString[] = [DAI, UNISWAP_V3_ROUTER, WETH] + const auths = targets.map((address, i) => + eoaCode7702SignAuthorization({ chainId: '0x1', address, nonce: `0x${i}` }, privateKey), + ) + + // ─── build & send your single 7702 tx ─────────────────────────────── + const batchData = batchContract.encodeFunctionData('executeBatch', [calls]) as `0x${string}` + + const txData: TxData = { + nonce: 0n, + gasLimit: 300_000n, + maxFeePerGas: 50_000_000_000n, + maxPriorityFeePerGas: 5_000_000_000n, + to: BATCH_CONTRACT, + value: 0n, + data: hexToBytes(batchData), + accessList: [], + authorizationList: auths, + } + + const tx = new EOACode7702Tx(txData, { common }).sign(privateKey) + const { execResult } = await runTx(vm, { tx }) + + console.log( + '🔀 Batch swap DAI→WETH → your wallet:', + execResult.exceptionError ? '❌ Failed' : '✅ Success', + ) +} + +run().catch(console.error) From 5eae393ac850fec026be40601353e235aa6194a6 Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 12:34:58 -0400 Subject: [PATCH 06/10] chore: cleanup --- .../7702/atomic-erc20-approve-transferFrom.ts | 286 ------------------ .../vm/examples/7702/uniswap-swap-transfer.ts | 6 +- .../7702/uniswap-swap-wrap-transfer.ts | 111 ------- packages/vm/examples/7702/uniswap-swap.ts | 252 --------------- 4 files changed, 3 insertions(+), 652 deletions(-) delete mode 100644 packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts delete mode 100644 packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts delete mode 100644 packages/vm/examples/7702/uniswap-swap.ts diff --git a/packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts b/packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts deleted file mode 100644 index e4b254e6261..00000000000 --- a/packages/vm/examples/7702/atomic-erc20-approve-transferFrom.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { RPCStateManager } from '@ethereumjs/statemanager' -import { Capability, EOACode7702Tx, TransactionType } from '@ethereumjs/tx' -import { - Address, - EOACode7702AuthorizationListBytesItem, - EOACode7702AuthorizationListItem, - PrefixedHexString, - bigIntToBytes, - bytesToHex, - createAddressFromPrivateKey, - createAddressFromString, - hexToBytes, - intToHex, -} from '@ethereumjs/util' -import { createVM } from '@ethereumjs/vm' -import { Contract } from 'ethers' -import { TxData } from '../../../tx/dist/esm/7702/tx' - -/** - * This example demonstrates how to use EIP-7702 to perform atomic ERC20 operations - * (approve + transferFrom) in a single transaction using RPCStateManager to - * simulate against a real network. - * - * WARNING: DO NOT USE REAL PRIVATE KEYS WITH VALUE. This is for demonstration only. - */ - -// ERC20 Interface -const erc20Abi = [ - 'function approve(address spender, uint256 amount) external returns (bool)', - 'function transferFrom(address sender, address recipient, uint256 amount) external returns (bool)', - 'function balanceOf(address account) external view returns (uint256)', - 'function allowance(address owner, address spender) external view returns (uint256)', -] - -// Bundle contract that handles atomic approve + transferFrom -// This is what an EOA will delegate to with EIP-7702 -const bundleContractCode = ` -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -interface IERC20 { - function approve(address spender, uint256 amount) external returns (bool); - function transferFrom(address from, address to, uint256 amount) external returns (bool); - function balanceOf(address account) external view returns (uint256); - function allowance(address owner, address spender) external view returns (uint256); -} - -contract ERC20Bundler { - /** - * @dev Atomically approves and transfers ERC20 tokens in a single call. - * @param token The ERC20 token address - * @param to The recipient address - * @param amount The amount to approve and transfer - * @return success True if the operation was successful - */ - function approveAndTransfer(address token, address to, uint256 amount) external returns (bool) { - // Approve the bundler contract to spend tokens - bool approved = IERC20(token).approve(address(this), amount); - require(approved, "Approval failed"); - - // Transfer the tokens from the caller to the recipient - bool transferred = IERC20(token).transferFrom(msg.sender, to, amount); - require(transferred, "Transfer failed"); - - return true; - } -} -` - -// Simulates a deployed bundle contract -const BUNDLE_CONTRACT_ADDRESS: PrefixedHexString = '0x1234567890123456789012345678901234567890' - -// DAI token on mainnet -const DAI_ADDRESS: PrefixedHexString = '0x6B175474E89094C44Da98b954EedeAC495271d0F' - -const runExample = async () => { - // For demonstration purposes, we're using a fake private key - // WARNING: Never use real private keys in code or examples - const privateKey = hexToBytes( - '0x1122334455667788112233445566778811223344556677881122334455667788', - ) - const userAddress = createAddressFromPrivateKey(privateKey) - - console.log('User address:', userAddress.toString()) - - // Initialize Common with EIP-7702 enabled - const common = new Common({ - chain: Mainnet, - hardfork: Hardfork.Cancun, - eips: [7702], - }) - - // We'll use RPCStateManager to interact with the real network state - // For this example we're using a local node, but you could use any provider - // This allows us to simulate transactions against real network state - const provider = 'http://localhost:8545' // Replace with an actual provider URL - - // Create a state manager with the required parameters - const rpcStateManager = new RPCStateManager({ - provider, - blockTag: 'earliest', // Using a valid value - }) - - // Create VM instance with the RPCStateManager - // Use the static create method of VM - const vm = await createVM({ - common, - stateManager: rpcStateManager, - }) - - // Check if user has a DAI balance - const daiContract = new Contract(DAI_ADDRESS, erc20Abi) - const balanceOfCalldata = daiContract.encodeFunctionData('balanceOf', [userAddress.toString()]) - - const balanceOfResult = await vm.evm.runCall({ - to: new Address(hexToBytes(DAI_ADDRESS)), - caller: userAddress, - data: hexToBytes(balanceOfCalldata), - }) - - // Decode the balance result - const daiBalance = - balanceOfResult.execResult.returnValue.length > 0 - ? daiContract.decodeFunctionResult( - 'balanceOf', - bytesToHex(balanceOfResult.execResult.returnValue), - )[0] - : 0n - - console.log('DAI balance:', daiBalance.toString()) - - if (daiBalance <= 0n) { - console.log('No DAI balance to demonstrate with') - return - } - - // Create an EIP-7702 transaction that will delegate the user's EOA - // to the bundle contract for this transaction - - // Recipient of the DAI transfer - const recipientAddress = new Address(hexToBytes('0x0000000000000000000000000000000000005678')) - console.log('Recipient address:', recipientAddress.toString()) - - // Amount to transfer (use a small amount for the demo) - const transferAmount = 1000000000000000000n // 1 DAI - - // Create the calldata for the bundle contract's approveAndTransfer function - const bundleInterface = new Interface([ - 'function approveAndTransfer(address token, address to, uint256 amount) external returns (bool)', - ]) - - const approveAndTransferCalldata = bundleInterface.encodeFunctionData('approveAndTransfer', [ - DAI_ADDRESS, - recipientAddress.toString(), - transferAmount, - ]) - - const authorizationListItem: EOACode7702AuthorizationListItem = { - chainId: bigcommon.chainId(), - address: BUNDLE_CONTRACT_ADDRESS, - nonce: intToHex(0), - yParity: intToHex(0), - r: `0x${'01'.repeat(32)}`, - s: `0x${'01'.repeat(32)}`, - } - - // Create the EIP-7702 transaction with authorization to use the bundle contract - const txData: TxData = { - nonce: 0n, - gasLimit: 300000n, - maxFeePerGas: 20000000000n, - maxPriorityFeePerGas: 2000000000n, - to: new Address(hexToBytes(BUNDLE_CONTRACT_ADDRESS)), - value: 0n, - data: hexToBytes(approveAndTransferCalldata), - accessList: [], - authorizationList: [ - { - chainId: common.chainId(), - address: new Address(hexToBytes(BUNDLE_CONTRACT_ADDRESS)), - nonce: 0n, - yParity: 0n, - r: hexToBytes('0x1234567890123456789012345678901234567890123456789012345678901234'), - s: hexToBytes('0x1234567890123456789012345678901234567890123456789012345678901234'), - }, - ], - } // Type assertion to bypass type checking - - // Pass common as a separate option - const tx = new EOACode7702Tx(txData, { common }) - const signedTx = tx.sign(privateKey) - - console.log('Transaction created successfully') - console.log('Transaction type:', TransactionType[signedTx.type]) - console.log('Supports EIP-7702:', signedTx.supports(Capability.EIP7702EOACode)) - - // Run the transaction to simulate what would happen - console.log('\nSimulating transaction...') - - try { - const result = await vm.runTx({ tx: signedTx }) - - console.log( - 'Transaction simulation:', - result.execResult.exceptionError !== null && result.execResult.exceptionError !== undefined - ? 'Failed' - : 'Success', - ) - - if ( - result.execResult.exceptionError === null || - result.execResult.exceptionError === undefined - ) { - console.log('Gas used:', result.gasUsed.toString()) - - // Check DAI allowance after the transaction - const allowanceCalldata = erc20Interface.encodeFunctionData('allowance', [ - userAddress.toString(), - BUNDLE_CONTRACT_ADDRESS, - ]) - - const allowanceResult = await vm.evm.runCall({ - to: new Address(hexToBytes(DAI_ADDRESS)), - caller: userAddress, - data: hexToBytes(allowanceCalldata), - }) - - const allowance = - allowanceResult.execResult.returnValue.length > 0 - ? erc20Interface.decodeFunctionResult( - 'allowance', - bytesToHex(allowanceResult.execResult.returnValue), - )[0] - : 0n - - console.log('DAI allowance after transaction:', allowance.toString()) - - // Check recipient's DAI balance after the transaction - const recipientBalanceCalldata = erc20Interface.encodeFunctionData('balanceOf', [ - recipientAddress.toString(), - ]) - - const recipientBalanceResult = await vm.evm.runCall({ - to: createAddressFromString(DAI_ADDRESS), - caller: userAddress, - data: hexToBytes(recipientBalanceCalldata), - }) - - const recipientBalance = - recipientBalanceResult.execResult.returnValue.length > 0 - ? erc20Interface.decodeFunctionResult( - 'balanceOf', - bytesToHex(recipientBalanceResult.execResult.returnValue), - )[0] - : 0n - - console.log('Recipient DAI balance after transaction:', recipientBalance.toString()) - - // Explain what happened - console.log('\nTransaction Summary:') - console.log('- User authorized their EOA to use the bundle contract implementation') - console.log('- The EOA executed the approveAndTransfer function which:') - console.log(' 1. Approved the bundle contract to spend DAI tokens') - console.log(' 2. Transferred DAI tokens to the recipient in a single atomic transaction') - console.log('\nThis demonstrates the power of EIP-7702 to enable advanced features for EOAs') - console.log( - 'without needing to deploy an account contract or switch to a smart contract wallet.', - ) - } else { - console.log('Error:', result.execResult.exceptionError.error) - } - } catch (error) { - console.error('Simulation error:', error) - } - - // This would be sent to the actual network using: - // const serializedTx = bytesToHex(signedTx.serialize()) - // console.log('Serialized transaction for broadcasting:', serializedTx) -} - -runExample().catch((error) => { - if (error !== null && error !== undefined) { - console.error('Error:', error) - } -}) diff --git a/packages/vm/examples/7702/uniswap-swap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-transfer.ts index 76a484b5b28..dcf2411e6af 100644 --- a/packages/vm/examples/7702/uniswap-swap-transfer.ts +++ b/packages/vm/examples/7702/uniswap-swap-transfer.ts @@ -84,9 +84,9 @@ async function run() { const txData: TxData = { nonce: 0n, - gasLimit: 300_000n, - maxFeePerGas: 50_000_000_000n, - maxPriorityFeePerGas: 5_000_000_000n, + gasLimit: 1_000_000n, + maxFeePerGas: parseUnits('10', 9), // 10 gwei + maxPriorityFeePerGas: parseUnits('5', 9), // 5 gwei to: BATCH_CONTRACT, value: 0n, data: hexToBytes(batchData), diff --git a/packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts deleted file mode 100644 index 8eb42220fda..00000000000 --- a/packages/vm/examples/7702/uniswap-swap-wrap-transfer.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Common, Hardfork, Mainnet } from '@ethereumjs/common' -import { RPCStateManager } from '@ethereumjs/statemanager' -import { EOACode7702Tx } from '@ethereumjs/tx' -import { - Address, - EOACode7702AuthorizationListItemUnsigned, - PrefixedHexString, - createAddressFromPrivateKey, - eoaCode7702SignAuthorization, - hexToBytes, -} from '@ethereumjs/util' -import { createVM, runTx } from '@ethereumjs/vm' -import { Interface, parseEther, parseUnits } from 'ethers' -import { TxData } from '../../../tx/dist/esm/7702/tx' - -async function runBatched7702() { - // ─── setup ────────────────────────────────────────────────────────────── - const privateKey = hexToBytes( - '0x1122334455667788112233445566778811223344556677881122334455667788', - ) - const userAddress = createAddressFromPrivateKey(privateKey) - console.log('EOA:', userAddress.toString()) - - const common = new Common({ - chain: Mainnet, - hardfork: Hardfork.Cancun, - eips: [7702], - }) - const stateManager = new RPCStateManager({ - provider: 'YOUR_PROVIDER_URL', - blockTag: 22_000_000n, // Example block number - }) - const vm = await createVM({ common, stateManager }) - - // ─── constants & ABIs ─────────────────────────────────────────────────── - const TOKEN = '0x6B175474E89094C44Da98b954EedeAC495271d0F' - const ROUTER = '0x66a9893cc07d91d95644aedd05d03f95e1dba8af' - const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' - const COLD_WALLET = `0x${'42'.repeat(20)}` - - const erc20Abi = ['function approve(address,uint256)'] - const routerAbi = ['function swapExactTokensForETH(uint256,uint256,address[],address,uint256)'] - const wethAbi = ['function deposit()', 'function transfer(address,uint256)'] - const batchAbi = ['function executeBatch(bytes[] calldata calls) external'] - - const erc20 = new Interface(erc20Abi) - const router = new Interface(routerAbi) - const weth = new Interface(wethAbi) - const batch = new Interface(batchAbi) - const BATCH_CONTRACT = '0xYourBatchContractAddressHere' - - // ─── parameters ───────────────────────────────────────────────────────── - const amountIn = parseUnits('1', 18) // 1 DAI - const amountOutMin = 0n - const deadline = BigInt(Math.floor(Date.now() / 1e3) + 20 * 60) - - // ─── build individual call-bytes ──────────────────────────────────────── - const callApprove = erc20.encodeFunctionData('approve', [ROUTER, amountIn]) as PrefixedHexString - - const callSwap = router.encodeFunctionData('swapExactTokensForETH', [ - amountIn, - amountOutMin, - [TOKEN, WETH], - userAddress.toString(), - deadline, - ]) as PrefixedHexString - - const callDeposit = weth.encodeFunctionData('deposit', []) as PrefixedHexString - const callTransfer = weth.encodeFunctionData('transfer', [ - COLD_WALLET, - parseEther('1'), - ]) as PrefixedHexString - - const calls = [callApprove, callSwap, callDeposit, callTransfer].map(hexToBytes) // bytes[] - - // ─── sign four 7702 authorizations ────────────────────────────────────── - const auths = [ - { address: TOKEN, nonce: '0x0' }, - { address: ROUTER, nonce: '0x1' }, - { address: WETH, nonce: '0x2' }, - { address: WETH, nonce: '0x3' }, - ].map(({ address, nonce }) => { - const unsigned: EOACode7702AuthorizationListItemUnsigned = { - chainId: '0x1', - address: address as PrefixedHexString, - nonce: nonce as PrefixedHexString, - } - return eoaCode7702SignAuthorization(unsigned, privateKey) - }) - - // ─── one single 7702 tx ───────────────────────────────────────────────── - const batchData = batch.encodeFunctionData('executeBatch', [calls]) as PrefixedHexString - - const txData: TxData = { - nonce: 0n, - gasLimit: 800_000n, - maxFeePerGas: 50_000_000_000n, - maxPriorityFeePerGas: 5_000_000_000n, - to: new Address(hexToBytes(BATCH_CONTRACT)), - value: 0n, - data: hexToBytes(batchData), - authorizationList: auths, - } - - const tx = new EOACode7702Tx(txData, { common }).sign(privateKey) - const result = await runTx(vm, { tx }) - - console.log('🔀 Batch 7702:', result.execResult.exceptionError ? '❌' : '✅') -} - -runBatched7702().catch(console.error) diff --git a/packages/vm/examples/7702/uniswap-swap.ts b/packages/vm/examples/7702/uniswap-swap.ts deleted file mode 100644 index b2bf702d9b0..00000000000 --- a/packages/vm/examples/7702/uniswap-swap.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { Chain, Common, Hardfork } from '@ethereumjs/common' -import { RPCStateManager } from '@ethereumjs/statemanager' -import { Capability, EOACode7702Tx, TransactionType } from '@ethereumjs/tx' -import { Address, bytesToHex, hexToBytes } from '@ethereumjs/util' -import { VM } from '@ethereumjs/vm' - -/** - * This example demonstrates how to use EIP-7702 to perform a Uniswap swap - * from an EOA using the RPCStateManager to simulate against a real network. - * - * WARNING: DO NOT USE REAL PRIVATE KEYS WITH VALUE. This is for demonstration only. - */ - -// Uniswap V2 Router address on Mainnet -const UNISWAP_ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' - -// Token addresses -const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F' -const WETH_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' - -// ABI snippets for the relevant functions -const ERC20_APPROVE_ABI = { - inputs: [ - { name: 'spender', type: 'address' }, - { name: 'amount', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', -} - -const UNISWAP_SWAP_ABI = { - inputs: [ - { name: 'amountIn', type: 'uint256' }, - { name: 'amountOutMin', type: 'uint256' }, - { name: 'path', type: 'address[]' }, - { name: 'to', type: 'address' }, - { name: 'deadline', type: 'uint256' }, - ], - name: 'swapExactTokensForTokens', - outputs: [{ name: 'amounts', type: 'uint256[]' }], - stateMutability: 'nonpayable', - type: 'function', -} - -// Bundler contract that combines approve and swap in one transaction -const uniswapBundlerCode = ` -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -interface IERC20 { - function approve(address spender, uint256 amount) external returns (bool); - function balanceOf(address account) external view returns (uint256); -} - -interface IUniswapV2Router { - function swapExactTokensForTokens( - uint amountIn, - uint amountOutMin, - address[] calldata path, - address to, - uint deadline - ) external returns (uint[] memory amounts); -} - -contract UniswapBundler { - /** - * @dev Atomically approves and swaps tokens using Uniswap in a single call - * @param tokenIn The input token address - * @param tokenOut The output token address - * @param amountIn The amount of input tokens to swap - * @param amountOutMin The minimum amount of output tokens to receive - * @param router The Uniswap router address - * @param deadline The deadline for the swap - * @return amounts The amounts of tokens exchanged - */ - function approveAndSwap( - address tokenIn, - address tokenOut, - uint256 amountIn, - uint256 amountOutMin, - address router, - uint256 deadline - ) external returns (uint256[] memory) { - // Approve the router to spend tokens - IERC20(tokenIn).approve(router, amountIn); - - // Create the swap path - address[] memory path = new address[](2); - path[0] = tokenIn; - path[1] = tokenOut; - - // Execute the swap - return IUniswapV2Router(router).swapExactTokensForTokens( - amountIn, - amountOutMin, - path, - msg.sender, // Send output tokens directly to the caller - deadline - ); - } -} -` - -// Simulates a deployed bundler contract -const BUNDLER_CONTRACT_ADDRESS = '0x1234567890123456789012345678901234567890' - -// Helper function to encode function call -function encodeFunction(abi: any, values: any[]): Uint8Array { - // This is a simplified version. In a real app, use ethers.js or web3.js - const signature = `${abi.name}(${abi.inputs.map((input: any) => input.type).join(',')})` - const functionSelector = hexToBytes(`0x${signature.slice(0, 10)}`) - - // This is just a placeholder - in a real implementation, you would properly ABI encode the parameters - console.log(`Encoded function call to ${signature}`) - return functionSelector -} - -// Create ABI for the bundler contract -const BUNDLER_ABI = { - inputs: [ - { name: 'tokenIn', type: 'address' }, - { name: 'tokenOut', type: 'address' }, - { name: 'amountIn', type: 'uint256' }, - { name: 'amountOutMin', type: 'uint256' }, - { name: 'router', type: 'address' }, - { name: 'deadline', type: 'uint256' }, - ], - name: 'approveAndSwap', - outputs: [{ name: 'amounts', type: 'uint256[]' }], - stateMutability: 'nonpayable', - type: 'function', -} - -const main = async () => { - // For demonstration purposes only, using fake keys - // WARNING: Never use real private keys in code - const privateKey = hexToBytes( - '0x1122334455667788112233445566778811223344556677881122334455667788', - ) - const userAddress = new Address(hexToBytes('0x1234567890123456789012345678901234567890')) - console.log('User address:', userAddress.toString()) - - // Initialize Common with EIP-7702 enabled - const common = new Common({ - chain: Chain.Mainnet as any, - hardfork: Hardfork.Cancun, - eips: [7702], - }) - - // We'll use RPCStateManager to interact with a mainnet fork - const provider = 'http://localhost:8545' - const blockTag = 'earliest' - const rpcStateManager = new RPCStateManager({ provider, blockTag }) - - // Create VM instance with the RPCStateManager - const vm = await (VM as any).create({ common, stateManager: rpcStateManager }) - - console.log('Simulating a Uniswap swap with EIP-7702...') - - // Parameters for the swap - const amountIn = 1000000000000000000n // 1 DAI - const amountOutMin = 1n // Accept any amount (in production, use price oracle for slippage protection) - const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 20) // 20 minutes from now - - // Create calldata for the bundler contract's approveAndSwap function - const calldata = encodeFunction(BUNDLER_ABI, [ - DAI_ADDRESS, - WETH_ADDRESS, - amountIn, - amountOutMin, - UNISWAP_ROUTER_ADDRESS, - deadline, - ]) - - // Create the EIP-7702 transaction - const txData = { - nonce: 0n, - gasLimit: 500000n, - maxFeePerGas: 30000000000n, // 30 gwei - maxPriorityFeePerGas: 3000000000n, // 3 gwei - to: new Address(hexToBytes(`0x${BUNDLER_CONTRACT_ADDRESS.slice(2)}` as `0x${string}`)), - value: 0n, - data: calldata, - accessList: [], - authorizationList: [ - { - chainId: common.chainId(), - address: new Address(hexToBytes(`0x${BUNDLER_CONTRACT_ADDRESS.slice(2)}` as `0x${string}`)), - nonce: 0n, - yParity: 0n, - r: hexToBytes( - '0x1234567890123456789012345678901234567890123456789012345678901234' as `0x${string}`, - ), - s: hexToBytes( - '0x1234567890123456789012345678901234567890123456789012345678901234' as `0x${string}`, - ), - }, - ] as any, - } - - // Create and sign the transaction - const tx = new EOACode7702Tx(txData, { common }) - const signedTx = tx.sign(privateKey) - - console.log('Transaction type:', TransactionType[signedTx.type]) - console.log('Supports EIP-7702:', signedTx.supports(Capability.EIP7702EOACode)) - - // Simulate the transaction - try { - console.log('Running transaction simulation...') - const result = await vm.runTx({ tx: signedTx }) - - console.log( - 'Transaction simulation:', - result.execResult.exceptionError !== null && result.execResult.exceptionError !== undefined - ? 'Failed' - : 'Success', - ) - - if ( - result.execResult.exceptionError === null || - result.execResult.exceptionError === undefined - ) { - console.log('Gas used:', result.gasUsed.toString()) - - console.log('\nTransaction Summary:') - console.log( - '- The EOA authorized delegation to the UniswapBundler contract for this transaction', - ) - console.log('- The bundler contract atomically executed:') - console.log(' 1. Approval of DAI tokens to the Uniswap router') - console.log(' 2. Swap of DAI for WETH using Uniswap') - console.log('\nBenefits of using EIP-7702 for this use case:') - console.log('- Saved gas by combining multiple transactions into one') - console.log('- Better UX with atomic approval and swap') - console.log('- No need to deploy a separate smart contract wallet') - console.log("- Maintained security of the user's EOA") - } else { - console.log('Error:', result.execResult.exceptionError.error) - } - } catch (error) { - console.error('Simulation error:', error) - } -} - -main().catch((error) => { - if (error !== null && error !== undefined) { - console.error('Error:', error) - } -}) From 880ffed65fe01b93d2133e58f93b8e9226d98bd3 Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 12:36:51 -0400 Subject: [PATCH 07/10] fix: lint --- packages/vm/examples/7702/uniswap-swap-transfer.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/vm/examples/7702/uniswap-swap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-transfer.ts index dcf2411e6af..4d4d47b4e24 100644 --- a/packages/vm/examples/7702/uniswap-swap-transfer.ts +++ b/packages/vm/examples/7702/uniswap-swap-transfer.ts @@ -1,16 +1,14 @@ import { Common, Hardfork, Mainnet } from '@ethereumjs/common' import { RPCStateManager } from '@ethereumjs/statemanager' -import { EOACode7702Tx } from '@ethereumjs/tx' +import { EOACode7702Tx, type EOACode7702TxData } from '@ethereumjs/tx' +import type { PrefixedHexString } from '@ethereumjs/util' import { - Address, - PrefixedHexString, createAddressFromPrivateKey, eoaCode7702SignAuthorization, hexToBytes, } from '@ethereumjs/util' import { createVM, runTx } from '@ethereumjs/vm' -import { AbiCoder, Interface, parseEther, parseUnits } from 'ethers' -import { TxData } from '../../../tx/dist/esm/7702/tx' +import { Interface, parseEther, parseUnits } from 'ethers' async function run() { // ─── your EOA key & address ─────────────────────────────────────────── @@ -82,7 +80,7 @@ async function run() { // ─── build & send your single 7702 tx ─────────────────────────────── const batchData = batchContract.encodeFunctionData('executeBatch', [calls]) as `0x${string}` - const txData: TxData = { + const txData: EOACode7702TxData = { nonce: 0n, gasLimit: 1_000_000n, maxFeePerGas: parseUnits('10', 9), // 10 gwei From 130350ec67b7d52bddacb0ae2e762f9b6b505516 Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Sun, 27 Apr 2025 13:01:06 -0400 Subject: [PATCH 08/10] chore: cleanup --- .../vm/examples/7702/uniswap-swap-transfer.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/vm/examples/7702/uniswap-swap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-transfer.ts index 4d4d47b4e24..4a1d3480da8 100644 --- a/packages/vm/examples/7702/uniswap-swap-transfer.ts +++ b/packages/vm/examples/7702/uniswap-swap-transfer.ts @@ -49,29 +49,32 @@ async function run() { // ─── trade parameters ──────────────────────────────────────────────── const amountIn = parseUnits('10000', 18) // 10000 DAI - const amountOutMin = parseEther('4') // expect at least 4 WETH + const amountOut = parseEther('4') // expect at least 4 WETH - // ─── encode your two sub-calls ─────────────────────────────────────── + // ─── encode your underlying swap data ─────────────────────────────────── + const uniswapV3SwapPayload = `0xYourSwapCallDataHere` + + // ─── encode your three sub-calls ─────────────────────────────────────── + // 1) DAI approve const callApprove = erc20.encodeFunctionData('approve', [ UNISWAP_V3_ROUTER, amountIn, ]) as PrefixedHexString - // ─── encode your swap call data ─────────────────────────────────── - const uniswapV3SwapCallData = `0xYourSwapCallDataHere` - + // 2) Uniswap V3 swapExactInput const callSwap = router.encodeFunctionData('exactInput', [ - uniswapV3SwapCallData, + uniswapV3SwapPayload, ]) as PrefixedHexString - const callTransfer = router.encodeFunctionData('transfer', [ + // 3) sweep WETH to cold wallet + const callTransfer = erc20.encodeFunctionData('transfer', [ COLD_WALLET, - amountOutMin, + amountOut, ]) as PrefixedHexString const calls = [callApprove, callSwap, callTransfer].map(hexToBytes) - // ─── sign one authorization each for DAI approve & router swap ────── + // ─── sign authorization for each ────── const targets: PrefixedHexString[] = [DAI, UNISWAP_V3_ROUTER, WETH] const auths = targets.map((address, i) => eoaCode7702SignAuthorization({ chainId: '0x1', address, nonce: `0x${i}` }, privateKey), From cd80ac716267414ac7369bc5864f01fd229f407b Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Mon, 28 Apr 2025 07:54:34 -0400 Subject: [PATCH 09/10] vm: address comments2 --- .../vm/examples/7702/uniswap-swap-transfer.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/vm/examples/7702/uniswap-swap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-transfer.ts index 4a1d3480da8..b53b6c6757d 100644 --- a/packages/vm/examples/7702/uniswap-swap-transfer.ts +++ b/packages/vm/examples/7702/uniswap-swap-transfer.ts @@ -20,8 +20,7 @@ async function run() { // ─── set up EthereumJS VM with EIP-7702 enabled ─────────────────────── const common = new Common({ chain: Mainnet, - hardfork: Hardfork.Cancun, - eips: [7702], + hardfork: Hardfork.Prague, }) const stateManager = new RPCStateManager({ provider: 'YourProviderURLHere', @@ -34,18 +33,21 @@ async function run() { const UNISWAP_V3_ROUTER = '0xE592427A0AEce92De3Edee1F18E0157C05861564' const WETH = '0xC02aaa39b223FE8D0A0e5c4F27EaD9083C756Cc2' const COLD_WALLET = `0x${'42'.repeat(20)}` - const BATCH_CONTRACT = '0xYourBatchContractAddressHere' const erc20Abi = [ 'function approve(address _spender, uint256 _amount) external returns (bool success)', 'function transfer(address _to, uint256 _value) public returns (bool success)', ] const routerAbi = ['function exactInput(bytes)'] - const batchAbi = ['function executeBatch(bytes[] calldata) external'] + + // This Batch contract is a placeholder. Replace with your actual batch contract address. + // This contract is responsible for executing multiple calls in a single TX. + const BATCH_CONTRACT = '0xYourBatchContractAddressHere' + const batchAbi = ['function executeBatch(bytes[] calldata, address[] targets) external'] const erc20 = new Interface(erc20Abi) const router = new Interface(routerAbi) - const batchContract = new Interface(batchAbi) + const batch = new Interface(batchAbi) // ─── trade parameters ──────────────────────────────────────────────── const amountIn = parseUnits('10000', 18) // 10000 DAI @@ -72,27 +74,28 @@ async function run() { amountOut, ]) as PrefixedHexString - const calls = [callApprove, callSwap, callTransfer].map(hexToBytes) - - // ─── sign authorization for each ────── const targets: PrefixedHexString[] = [DAI, UNISWAP_V3_ROUTER, WETH] - const auths = targets.map((address, i) => - eoaCode7702SignAuthorization({ chainId: '0x1', address, nonce: `0x${i}` }, privateKey), - ) + const calls = [callApprove, callSwap, callTransfer].map(hexToBytes) // ─── build & send your single 7702 tx ─────────────────────────────── - const batchData = batchContract.encodeFunctionData('executeBatch', [calls]) as `0x${string}` + const batchData = batch.encodeFunctionData('executeBatch', [calls, targets]) as `0x${string}` + + // ─── sign authorization for Batch Contract ────── + const authorizationListItem = eoaCode7702SignAuthorization( + { chainId: '0x1', address: BATCH_CONTRACT, nonce: `0x$1` }, + privateKey, + ) const txData: EOACode7702TxData = { nonce: 0n, gasLimit: 1_000_000n, maxFeePerGas: parseUnits('10', 9), // 10 gwei maxPriorityFeePerGas: parseUnits('5', 9), // 5 gwei - to: BATCH_CONTRACT, + to: userAddress, // Using our own wallet, which will be acting as the BATCH_CONTRACT smart contract value: 0n, data: hexToBytes(batchData), accessList: [], - authorizationList: auths, + authorizationList: [authorizationListItem], } const tx = new EOACode7702Tx(txData, { common }).sign(privateKey) From 73f8ee496a51f81e45d43f15e739c30bf27a811b Mon Sep 17 00:00:00 2001 From: Gabriel Rocheleau Date: Mon, 28 Apr 2025 08:07:21 -0400 Subject: [PATCH 10/10] chore: adjust nonce --- packages/vm/examples/7702/uniswap-swap-transfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vm/examples/7702/uniswap-swap-transfer.ts b/packages/vm/examples/7702/uniswap-swap-transfer.ts index b53b6c6757d..e34000b4590 100644 --- a/packages/vm/examples/7702/uniswap-swap-transfer.ts +++ b/packages/vm/examples/7702/uniswap-swap-transfer.ts @@ -82,7 +82,7 @@ async function run() { // ─── sign authorization for Batch Contract ────── const authorizationListItem = eoaCode7702SignAuthorization( - { chainId: '0x1', address: BATCH_CONTRACT, nonce: `0x$1` }, + { chainId: '0x1', address: BATCH_CONTRACT, nonce: `0x1` }, privateKey, )