Skip to content

Commit 723e789

Browse files
examples: atomic approve-transferFrom + uniswap swap
Co-authored-by: Avdhesh Charjan <[email protected]>
1 parent 8d76c4b commit 723e789

File tree

2 files changed

+538
-0
lines changed

2 files changed

+538
-0
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import { Common, Hardfork, Mainnet } from '@ethereumjs/common'
2+
import { RPCStateManager } from '@ethereumjs/statemanager'
3+
import { Capability, EOACode7702Tx, TransactionType } from '@ethereumjs/tx'
4+
import {
5+
Address,
6+
EOACode7702AuthorizationListBytesItem,
7+
EOACode7702AuthorizationListItem,
8+
PrefixedHexString,
9+
bigIntToBytes,
10+
bytesToHex,
11+
createAddressFromPrivateKey,
12+
createAddressFromString,
13+
hexToBytes,
14+
intToHex,
15+
} from '@ethereumjs/util'
16+
import { createVM } from '@ethereumjs/vm'
17+
import { Contract } from 'ethers'
18+
import { TxData } from '../../../tx/dist/esm/7702/tx'
19+
20+
/**
21+
* This example demonstrates how to use EIP-7702 to perform atomic ERC20 operations
22+
* (approve + transferFrom) in a single transaction using RPCStateManager to
23+
* simulate against a real network.
24+
*
25+
* WARNING: DO NOT USE REAL PRIVATE KEYS WITH VALUE. This is for demonstration only.
26+
*/
27+
28+
// ERC20 Interface
29+
const erc20Abi = [
30+
'function approve(address spender, uint256 amount) external returns (bool)',
31+
'function transferFrom(address sender, address recipient, uint256 amount) external returns (bool)',
32+
'function balanceOf(address account) external view returns (uint256)',
33+
'function allowance(address owner, address spender) external view returns (uint256)',
34+
]
35+
36+
// Bundle contract that handles atomic approve + transferFrom
37+
// This is what an EOA will delegate to with EIP-7702
38+
const bundleContractCode = `
39+
// SPDX-License-Identifier: MIT
40+
pragma solidity ^0.8.19;
41+
42+
interface IERC20 {
43+
function approve(address spender, uint256 amount) external returns (bool);
44+
function transferFrom(address from, address to, uint256 amount) external returns (bool);
45+
function balanceOf(address account) external view returns (uint256);
46+
function allowance(address owner, address spender) external view returns (uint256);
47+
}
48+
49+
contract ERC20Bundler {
50+
/**
51+
* @dev Atomically approves and transfers ERC20 tokens in a single call.
52+
* @param token The ERC20 token address
53+
* @param to The recipient address
54+
* @param amount The amount to approve and transfer
55+
* @return success True if the operation was successful
56+
*/
57+
function approveAndTransfer(address token, address to, uint256 amount) external returns (bool) {
58+
// Approve the bundler contract to spend tokens
59+
bool approved = IERC20(token).approve(address(this), amount);
60+
require(approved, "Approval failed");
61+
62+
// Transfer the tokens from the caller to the recipient
63+
bool transferred = IERC20(token).transferFrom(msg.sender, to, amount);
64+
require(transferred, "Transfer failed");
65+
66+
return true;
67+
}
68+
}
69+
`
70+
71+
// Simulates a deployed bundle contract
72+
const BUNDLE_CONTRACT_ADDRESS: PrefixedHexString = '0x1234567890123456789012345678901234567890'
73+
74+
// DAI token on mainnet
75+
const DAI_ADDRESS: PrefixedHexString = '0x6B175474E89094C44Da98b954EedeAC495271d0F'
76+
77+
const runExample = async () => {
78+
// For demonstration purposes, we're using a fake private key
79+
// WARNING: Never use real private keys in code or examples
80+
const privateKey = hexToBytes(
81+
'0x1122334455667788112233445566778811223344556677881122334455667788',
82+
)
83+
const userAddress = createAddressFromPrivateKey(privateKey)
84+
85+
console.log('User address:', userAddress.toString())
86+
87+
// Initialize Common with EIP-7702 enabled
88+
const common = new Common({
89+
chain: Mainnet,
90+
hardfork: Hardfork.Cancun,
91+
eips: [7702],
92+
})
93+
94+
// We'll use RPCStateManager to interact with the real network state
95+
// For this example we're using a local node, but you could use any provider
96+
// This allows us to simulate transactions against real network state
97+
const provider = 'http://localhost:8545' // Replace with an actual provider URL
98+
99+
// Create a state manager with the required parameters
100+
const rpcStateManager = new RPCStateManager({
101+
provider,
102+
blockTag: 'earliest', // Using a valid value
103+
})
104+
105+
// Create VM instance with the RPCStateManager
106+
// Use the static create method of VM
107+
const vm = await createVM({
108+
common,
109+
stateManager: rpcStateManager,
110+
})
111+
112+
// Check if user has a DAI balance
113+
const daiContract = new Contract(DAI_ADDRESS, erc20Abi)
114+
const balanceOfCalldata = daiContract.encodeFunctionData('balanceOf', [userAddress.toString()])
115+
116+
const balanceOfResult = await vm.evm.runCall({
117+
to: new Address(hexToBytes(DAI_ADDRESS)),
118+
caller: userAddress,
119+
data: hexToBytes(balanceOfCalldata),
120+
})
121+
122+
// Decode the balance result
123+
const daiBalance =
124+
balanceOfResult.execResult.returnValue.length > 0
125+
? daiContract.decodeFunctionResult(
126+
'balanceOf',
127+
bytesToHex(balanceOfResult.execResult.returnValue),
128+
)[0]
129+
: 0n
130+
131+
console.log('DAI balance:', daiBalance.toString())
132+
133+
if (daiBalance <= 0n) {
134+
console.log('No DAI balance to demonstrate with')
135+
return
136+
}
137+
138+
// Create an EIP-7702 transaction that will delegate the user's EOA
139+
// to the bundle contract for this transaction
140+
141+
// Recipient of the DAI transfer
142+
const recipientAddress = new Address(hexToBytes('0x0000000000000000000000000000000000005678'))
143+
console.log('Recipient address:', recipientAddress.toString())
144+
145+
// Amount to transfer (use a small amount for the demo)
146+
const transferAmount = 1000000000000000000n // 1 DAI
147+
148+
// Create the calldata for the bundle contract's approveAndTransfer function
149+
const bundleInterface = new Interface([
150+
'function approveAndTransfer(address token, address to, uint256 amount) external returns (bool)',
151+
])
152+
153+
const approveAndTransferCalldata = bundleInterface.encodeFunctionData('approveAndTransfer', [
154+
DAI_ADDRESS,
155+
recipientAddress.toString(),
156+
transferAmount,
157+
])
158+
159+
const authorizationListItem: EOACode7702AuthorizationListItem = {
160+
chainId: bigcommon.chainId(),
161+
address: BUNDLE_CONTRACT_ADDRESS,
162+
nonce: intToHex(0),
163+
yParity: intToHex(0),
164+
r: `0x${'01'.repeat(32)}`,
165+
s: `0x${'01'.repeat(32)}`,
166+
}
167+
168+
// Create the EIP-7702 transaction with authorization to use the bundle contract
169+
const txData: TxData = {
170+
nonce: 0n,
171+
gasLimit: 300000n,
172+
maxFeePerGas: 20000000000n,
173+
maxPriorityFeePerGas: 2000000000n,
174+
to: new Address(hexToBytes(BUNDLE_CONTRACT_ADDRESS)),
175+
value: 0n,
176+
data: hexToBytes(approveAndTransferCalldata),
177+
accessList: [],
178+
authorizationList: [
179+
{
180+
chainId: common.chainId(),
181+
address: new Address(hexToBytes(BUNDLE_CONTRACT_ADDRESS)),
182+
nonce: 0n,
183+
yParity: 0n,
184+
r: hexToBytes('0x1234567890123456789012345678901234567890123456789012345678901234'),
185+
s: hexToBytes('0x1234567890123456789012345678901234567890123456789012345678901234'),
186+
},
187+
],
188+
} // Type assertion to bypass type checking
189+
190+
// Pass common as a separate option
191+
const tx = new EOACode7702Tx(txData, { common })
192+
const signedTx = tx.sign(privateKey)
193+
194+
console.log('Transaction created successfully')
195+
console.log('Transaction type:', TransactionType[signedTx.type])
196+
console.log('Supports EIP-7702:', signedTx.supports(Capability.EIP7702EOACode))
197+
198+
// Run the transaction to simulate what would happen
199+
console.log('\nSimulating transaction...')
200+
201+
try {
202+
const result = await vm.runTx({ tx: signedTx })
203+
204+
console.log(
205+
'Transaction simulation:',
206+
result.execResult.exceptionError !== null && result.execResult.exceptionError !== undefined
207+
? 'Failed'
208+
: 'Success',
209+
)
210+
211+
if (
212+
result.execResult.exceptionError === null ||
213+
result.execResult.exceptionError === undefined
214+
) {
215+
console.log('Gas used:', result.gasUsed.toString())
216+
217+
// Check DAI allowance after the transaction
218+
const allowanceCalldata = erc20Interface.encodeFunctionData('allowance', [
219+
userAddress.toString(),
220+
BUNDLE_CONTRACT_ADDRESS,
221+
])
222+
223+
const allowanceResult = await vm.evm.runCall({
224+
to: new Address(hexToBytes(DAI_ADDRESS)),
225+
caller: userAddress,
226+
data: hexToBytes(allowanceCalldata),
227+
})
228+
229+
const allowance =
230+
allowanceResult.execResult.returnValue.length > 0
231+
? erc20Interface.decodeFunctionResult(
232+
'allowance',
233+
bytesToHex(allowanceResult.execResult.returnValue),
234+
)[0]
235+
: 0n
236+
237+
console.log('DAI allowance after transaction:', allowance.toString())
238+
239+
// Check recipient's DAI balance after the transaction
240+
const recipientBalanceCalldata = erc20Interface.encodeFunctionData('balanceOf', [
241+
recipientAddress.toString(),
242+
])
243+
244+
const recipientBalanceResult = await vm.evm.runCall({
245+
to: createAddressFromString(DAI_ADDRESS),
246+
caller: userAddress,
247+
data: hexToBytes(recipientBalanceCalldata),
248+
})
249+
250+
const recipientBalance =
251+
recipientBalanceResult.execResult.returnValue.length > 0
252+
? erc20Interface.decodeFunctionResult(
253+
'balanceOf',
254+
bytesToHex(recipientBalanceResult.execResult.returnValue),
255+
)[0]
256+
: 0n
257+
258+
console.log('Recipient DAI balance after transaction:', recipientBalance.toString())
259+
260+
// Explain what happened
261+
console.log('\nTransaction Summary:')
262+
console.log('- User authorized their EOA to use the bundle contract implementation')
263+
console.log('- The EOA executed the approveAndTransfer function which:')
264+
console.log(' 1. Approved the bundle contract to spend DAI tokens')
265+
console.log(' 2. Transferred DAI tokens to the recipient in a single atomic transaction')
266+
console.log('\nThis demonstrates the power of EIP-7702 to enable advanced features for EOAs')
267+
console.log(
268+
'without needing to deploy an account contract or switch to a smart contract wallet.',
269+
)
270+
} else {
271+
console.log('Error:', result.execResult.exceptionError.error)
272+
}
273+
} catch (error) {
274+
console.error('Simulation error:', error)
275+
}
276+
277+
// This would be sent to the actual network using:
278+
// const serializedTx = bytesToHex(signedTx.serialize())
279+
// console.log('Serialized transaction for broadcasting:', serializedTx)
280+
}
281+
282+
runExample().catch((error) => {
283+
if (error !== null && error !== undefined) {
284+
console.error('Error:', error)
285+
}
286+
})

0 commit comments

Comments
 (0)