Skip to content
Merged
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
5 changes: 4 additions & 1 deletion foundry.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"rev": "abff2aca61a8f0934e533d0d352978055fddbd96"
},
"lib/forge-std": {
"rev": "1fd874f0efdb711cb6807c4f4a000ed2805dc809"
"tag": {
"name": "v1.10.0",
"rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824"
}
},
"lib/openzeppelin-contracts": {
"rev": "5705e8208bc92cd82c7bcdfeac8dbc7377767d96"
Expand Down
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 55 files
+1 −0 .github/CODEOWNERS
+57 −109 .github/workflows/ci.yml
+3 −1 .github/workflows/sync.yml
+0 −3 .gitmodules
+193 −0 CONTRIBUTING.md
+21 −5 README.md
+5 −3 foundry.toml
+0 −1 lib/ds-test
+1 −1 package.json
+13 −2 scripts/vm.py
+16 −9 src/Base.sol
+1 −0 src/Script.sol
+590 −202 src/StdAssertions.sol
+49 −11 src/StdChains.sol
+17 −5 src/StdCheats.sol
+30 −0 src/StdConstants.sol
+18 −3 src/StdInvariant.sol
+111 −11 src/StdJson.sol
+202 −107 src/StdStorage.sol
+283 −0 src/StdToml.sol
+1 −19 src/StdUtils.sol
+5 −4 src/Test.sol
+1,416 −62 src/Vm.sol
+635 −608 src/console.sol
+1 −1,555 src/console2.sol
+1 −1 src/interfaces/IERC1155.sol
+3 −3 src/interfaces/IERC4626.sol
+72 −0 src/interfaces/IERC6909.sol
+1 −1 src/interfaces/IERC721.sol
+150 −0 src/interfaces/IERC7540.sol
+241 −0 src/interfaces/IERC7575.sol
+0 −216 src/mocks/MockERC20.sol
+0 −221 src/mocks/MockERC721.sol
+693 −4 src/safeconsole.sol
+44 −0 test/CommonBase.t.sol
+35 −909 test/StdAssertions.t.sol
+55 −44 test/StdChains.t.sol
+66 −37 test/StdCheats.t.sol
+38 −0 test/StdConstants.t.sol
+12 −12 test/StdError.t.sol
+49 −0 test/StdJson.t.sol
+12 −22 test/StdMath.t.sol
+190 −17 test/StdStorage.t.sol
+1 −1 test/StdStyle.t.sol
+49 −0 test/StdToml.t.sol
+26 −26 test/StdUtils.t.sol
+9 −6 test/Vm.t.sol
+1 −1 test/compilation/CompilationScript.sol
+1 −1 test/compilation/CompilationScriptBase.sol
+1 −1 test/compilation/CompilationTest.sol
+1 −1 test/compilation/CompilationTestBase.sol
+8 −0 test/fixtures/test.json
+6 −0 test/fixtures/test.toml
+0 −441 test/mocks/MockERC20.t.sol
+0 −721 test/mocks/MockERC721.t.sol
2 changes: 1 addition & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
FreshCryptoLib/=lib/webauthn-sol/lib/FreshCryptoLib/solidity/src/
account-abstraction/=lib/account-abstraction/contracts/
ds-test/=lib/forge-std/lib/ds-test/src/
ds-test/=lib/p256-verifier/lib/forge-std/lib/ds-test/src/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
Expand Down
6 changes: 6 additions & 0 deletions snapshots/EndToEndTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"e2e_transfer_erc20_baseAccount": "142302",
"e2e_transfer_erc20_eoa": "39910",
"e2e_transfer_native_baseAccount": "134635",
"e2e_transfer_native_eoa": "9338"
}
143 changes: 143 additions & 0 deletions test/gas/EndToEnd.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {console2} from "forge-std/Test.sol";
import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";

import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol";
import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol";
import {MockERC20} from "../../lib/solady/test/utils/mocks/MockERC20.sol";

import {MockTarget} from "../mocks/MockTarget.sol";
import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol";
import {Static} from "../CoinbaseSmartWallet/Static.sol";

/// @title EndToEndTest
/// @notice Gas comparison tests between ERC-4337 Base Account and EOA transactions
/// @dev Isolated test contract to measure gas consumption for common operations
/// Tests ran using `FOUNDRY_PROFILE=deploy` to simulate real-world gas costs
/// forge-config: default.isolate = true
contract EndToEndTest is SmartWalletTestBase {
address eoaUser = address(0xe0a);
MockERC20 usdc;
MockTarget target;
CoinbaseSmartWalletFactory factory;

function setUp() public override {
// Deploy EntryPoint at canonical address
vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES);

// Deploy smart wallet infrastructure
CoinbaseSmartWallet implementation = new CoinbaseSmartWallet();
factory = new CoinbaseSmartWalletFactory(address(implementation));

// Configure wallet owner
signerPrivateKey = 0xa11ce;
signer = vm.addr(signerPrivateKey);
owners.push(abi.encode(signer));
account = factory.createAccount(owners, 0);

// Fund wallets with ETH
vm.deal(address(account), 100 ether);
vm.deal(eoaUser, 100 ether);

// Deploy and mint USDC tokens
usdc = new MockERC20("USD Coin", "USDC", 6);
usdc.mint(address(account), 10000e6);
usdc.mint(eoaUser, 10000e6);

target = new MockTarget();
}

// Native ETH Transfer - Base Account
function test_transfer_native_baseAccount() public {
// Dust recipient to avoid gas changes for first non-zero balance
vm.deal(address(0x1234), 1 wei);

// Prepare UserOperation for native ETH transfer
userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.execute, (address(0x1234), 1 ether, ""));
UserOperation memory op = _getUserOpWithSignature();

// Measure calldata size
bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler)));
console2.log("test_transfer_native Base Account calldata size:", handleOpsCalldata.length);

// Execute and measure gas
vm.startSnapshotGas("e2e_transfer_native_baseAccount");
_sendUserOperation(op);
uint256 gasUsed = vm.stopSnapshotGas();
console2.log("test_transfer_native Base Account gas:", gasUsed);
}

// Native ETH Transfer - EOA
function test_transfer_native_eoa() public {
// Dust recipient to avoid gas changes for first non-zero balance
vm.deal(address(0x1234), 1 wei);

console2.log("test_transfer_native EOA calldata size:", uint256(0));

// Execute and measure gas
vm.prank(eoaUser);
vm.startSnapshotGas("e2e_transfer_native_eoa");
payable(address(0x1234)).transfer(1 ether);
uint256 gasUsed = vm.stopSnapshotGas();
console2.log("test_transfer_native EOA gas:", gasUsed);
}

// ERC20 Transfer - Base Account
function test_transfer_erc20_baseAccount() public {
// Dust recipient to avoid gas changes for first non-zero balance
vm.deal(address(0x5678), 1 wei);
usdc.mint(address(0x5678), 1 wei);

// Prepare UserOperation for ERC20 transfer
userOpCalldata = abi.encodeCall(
CoinbaseSmartWallet.execute, (address(usdc), 0, abi.encodeCall(usdc.transfer, (address(0x5678), 100e6)))
);
UserOperation memory op = _getUserOpWithSignature();

// Measure calldata size
bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler)));
console2.log("test_transfer_erc20 Base Account calldata size:", handleOpsCalldata.length);

// Execute and measure gas
vm.startSnapshotGas("e2e_transfer_erc20_baseAccount");
_sendUserOperation(op);
uint256 gasUsed = vm.stopSnapshotGas();
console2.log("test_transfer_erc20 Base Account gas:", gasUsed);
}

// ERC20 Transfer - EOA
function test_transfer_erc20_eoa() public {
// Dust recipient to avoid gas changes for first non-zero balance
vm.deal(address(0x5678), 1 wei);
usdc.mint(address(0x5678), 1 wei);

// Measure calldata size
bytes memory eoaCalldata = abi.encodeCall(usdc.transfer, (address(0x5678), 100e6));
console2.log("test_transfer_erc20 EOA calldata size:", eoaCalldata.length);

// Execute and measure gas
vm.prank(eoaUser);
vm.startSnapshotGas("e2e_transfer_erc20_eoa");
usdc.transfer(address(0x5678), 100e6);
uint256 gasUsed = vm.stopSnapshotGas();
console2.log("test_transfer_erc20 EOA gas:", gasUsed);
}

// Helper Functions
// Creates an array containing a single UserOperation
function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) {
UserOperation[] memory ops = new UserOperation[](1);
ops[0] = op;
return ops;
}

// Signs a UserOperation with the configured signer
// Overrides the parent implementation to use the configured signer
function _sign(UserOperation memory userOp) internal view override returns (bytes memory signature) {
bytes32 toSign = entryPoint.getUserOpHash(userOp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, toSign);
signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v)));
}
}
Loading