Skip to content

Commit 6d0c100

Browse files
authored
feat: E2E gas profiling native and erc20 (#127)
* added e2e tests * changed import ordering
1 parent 022b87b commit 6d0c100

File tree

5 files changed

+155
-3
lines changed

5 files changed

+155
-3
lines changed

foundry.lock

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"rev": "abff2aca61a8f0934e533d0d352978055fddbd96"
44
},
55
"lib/forge-std": {
6-
"rev": "1fd874f0efdb711cb6807c4f4a000ed2805dc809"
6+
"tag": {
7+
"name": "v1.10.0",
8+
"rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824"
9+
}
710
},
811
"lib/openzeppelin-contracts": {
912
"rev": "5705e8208bc92cd82c7bcdfeac8dbc7377767d96"

remappings.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
22
FreshCryptoLib/=lib/webauthn-sol/lib/FreshCryptoLib/solidity/src/
33
account-abstraction/=lib/account-abstraction/contracts/
4-
ds-test/=lib/forge-std/lib/ds-test/src/
4+
ds-test/=lib/p256-verifier/lib/forge-std/lib/ds-test/src/
55
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
66
forge-std/=lib/forge-std/src/
77
openzeppelin-contracts/=lib/openzeppelin-contracts/

snapshots/EndToEndTest.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"e2e_transfer_erc20_baseAccount": "142302",
3+
"e2e_transfer_erc20_eoa": "39910",
4+
"e2e_transfer_native_baseAccount": "134635",
5+
"e2e_transfer_native_eoa": "9338"
6+
}

test/gas/EndToEnd.t.sol

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
import {console2} from "forge-std/Test.sol";
5+
import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";
6+
7+
import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol";
8+
import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol";
9+
import {MockERC20} from "../../lib/solady/test/utils/mocks/MockERC20.sol";
10+
11+
import {MockTarget} from "../mocks/MockTarget.sol";
12+
import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol";
13+
import {Static} from "../CoinbaseSmartWallet/Static.sol";
14+
15+
/// @title EndToEndTest
16+
/// @notice Gas comparison tests between ERC-4337 Base Account and EOA transactions
17+
/// @dev Isolated test contract to measure gas consumption for common operations
18+
/// Tests ran using `FOUNDRY_PROFILE=deploy` to simulate real-world gas costs
19+
/// forge-config: default.isolate = true
20+
contract EndToEndTest is SmartWalletTestBase {
21+
address eoaUser = address(0xe0a);
22+
MockERC20 usdc;
23+
MockTarget target;
24+
CoinbaseSmartWalletFactory factory;
25+
26+
function setUp() public override {
27+
// Deploy EntryPoint at canonical address
28+
vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES);
29+
30+
// Deploy smart wallet infrastructure
31+
CoinbaseSmartWallet implementation = new CoinbaseSmartWallet();
32+
factory = new CoinbaseSmartWalletFactory(address(implementation));
33+
34+
// Configure wallet owner
35+
signerPrivateKey = 0xa11ce;
36+
signer = vm.addr(signerPrivateKey);
37+
owners.push(abi.encode(signer));
38+
account = factory.createAccount(owners, 0);
39+
40+
// Fund wallets with ETH
41+
vm.deal(address(account), 100 ether);
42+
vm.deal(eoaUser, 100 ether);
43+
44+
// Deploy and mint USDC tokens
45+
usdc = new MockERC20("USD Coin", "USDC", 6);
46+
usdc.mint(address(account), 10000e6);
47+
usdc.mint(eoaUser, 10000e6);
48+
49+
target = new MockTarget();
50+
}
51+
52+
// Native ETH Transfer - Base Account
53+
function test_transfer_native_baseAccount() public {
54+
// Dust recipient to avoid gas changes for first non-zero balance
55+
vm.deal(address(0x1234), 1 wei);
56+
57+
// Prepare UserOperation for native ETH transfer
58+
userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.execute, (address(0x1234), 1 ether, ""));
59+
UserOperation memory op = _getUserOpWithSignature();
60+
61+
// Measure calldata size
62+
bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler)));
63+
console2.log("test_transfer_native Base Account calldata size:", handleOpsCalldata.length);
64+
65+
// Execute and measure gas
66+
vm.startSnapshotGas("e2e_transfer_native_baseAccount");
67+
_sendUserOperation(op);
68+
uint256 gasUsed = vm.stopSnapshotGas();
69+
console2.log("test_transfer_native Base Account gas:", gasUsed);
70+
}
71+
72+
// Native ETH Transfer - EOA
73+
function test_transfer_native_eoa() public {
74+
// Dust recipient to avoid gas changes for first non-zero balance
75+
vm.deal(address(0x1234), 1 wei);
76+
77+
console2.log("test_transfer_native EOA calldata size:", uint256(0));
78+
79+
// Execute and measure gas
80+
vm.prank(eoaUser);
81+
vm.startSnapshotGas("e2e_transfer_native_eoa");
82+
payable(address(0x1234)).transfer(1 ether);
83+
uint256 gasUsed = vm.stopSnapshotGas();
84+
console2.log("test_transfer_native EOA gas:", gasUsed);
85+
}
86+
87+
// ERC20 Transfer - Base Account
88+
function test_transfer_erc20_baseAccount() public {
89+
// Dust recipient to avoid gas changes for first non-zero balance
90+
vm.deal(address(0x5678), 1 wei);
91+
usdc.mint(address(0x5678), 1 wei);
92+
93+
// Prepare UserOperation for ERC20 transfer
94+
userOpCalldata = abi.encodeCall(
95+
CoinbaseSmartWallet.execute, (address(usdc), 0, abi.encodeCall(usdc.transfer, (address(0x5678), 100e6)))
96+
);
97+
UserOperation memory op = _getUserOpWithSignature();
98+
99+
// Measure calldata size
100+
bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler)));
101+
console2.log("test_transfer_erc20 Base Account calldata size:", handleOpsCalldata.length);
102+
103+
// Execute and measure gas
104+
vm.startSnapshotGas("e2e_transfer_erc20_baseAccount");
105+
_sendUserOperation(op);
106+
uint256 gasUsed = vm.stopSnapshotGas();
107+
console2.log("test_transfer_erc20 Base Account gas:", gasUsed);
108+
}
109+
110+
// ERC20 Transfer - EOA
111+
function test_transfer_erc20_eoa() public {
112+
// Dust recipient to avoid gas changes for first non-zero balance
113+
vm.deal(address(0x5678), 1 wei);
114+
usdc.mint(address(0x5678), 1 wei);
115+
116+
// Measure calldata size
117+
bytes memory eoaCalldata = abi.encodeCall(usdc.transfer, (address(0x5678), 100e6));
118+
console2.log("test_transfer_erc20 EOA calldata size:", eoaCalldata.length);
119+
120+
// Execute and measure gas
121+
vm.prank(eoaUser);
122+
vm.startSnapshotGas("e2e_transfer_erc20_eoa");
123+
usdc.transfer(address(0x5678), 100e6);
124+
uint256 gasUsed = vm.stopSnapshotGas();
125+
console2.log("test_transfer_erc20 EOA gas:", gasUsed);
126+
}
127+
128+
// Helper Functions
129+
// Creates an array containing a single UserOperation
130+
function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) {
131+
UserOperation[] memory ops = new UserOperation[](1);
132+
ops[0] = op;
133+
return ops;
134+
}
135+
136+
// Signs a UserOperation with the configured signer
137+
// Overrides the parent implementation to use the configured signer
138+
function _sign(UserOperation memory userOp) internal view override returns (bytes memory signature) {
139+
bytes32 toSign = entryPoint.getUserOpHash(userOp);
140+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, toSign);
141+
signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v)));
142+
}
143+
}

0 commit comments

Comments
 (0)