From edc248b0c579e0b8886c8b314729c414d3ff8df3 Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Thu, 4 Sep 2025 10:37:43 -0700 Subject: [PATCH 1/8] init --- foundry.lock | 26 +++ lib/forge-std | 2 +- snapshots/ExecuteBatch.json | 7 + .../Execute_ETHTransfer_GasBenchmark.json | 3 + test/gas/ExecuteGasBenchmark.t.sol | 152 ++++++++++++++++++ 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 foundry.lock create mode 100644 snapshots/ExecuteBatch.json create mode 100644 snapshots/Execute_ETHTransfer_GasBenchmark.json create mode 100644 test/gas/ExecuteGasBenchmark.t.sol diff --git a/foundry.lock b/foundry.lock new file mode 100644 index 0000000..4be77fe --- /dev/null +++ b/foundry.lock @@ -0,0 +1,26 @@ +{ + "lib/account-abstraction": { + "rev": "abff2aca61a8f0934e533d0d352978055fddbd96" + }, + "lib/forge-std": { + "tag": { + "name": "v1.10.0", + "rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824" + } + }, + "lib/openzeppelin-contracts": { + "rev": "5705e8208bc92cd82c7bcdfeac8dbc7377767d96" + }, + "lib/p256-verifier": { + "rev": "29475ae300ec95d98d5c7cc34c094846f0aa2dcd" + }, + "lib/safe-singleton-deployer-sol": { + "rev": "cf2b89c33fed536c4dd6fef2fb84f39053068868" + }, + "lib/solady": { + "rev": "c4c96607cb3aa3807b14c81ae2015bcba061f8fc" + }, + "lib/webauthn-sol": { + "rev": "619f20ab0f074fef41066ee4ab24849a913263b2" + } +} \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index 1fd874f..8bbcf6e 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1fd874f0efdb711cb6807c4f4a000ed2805dc809 +Subproject commit 8bbcf6e3f8f62f419e5429a0bd89331c85c37824 diff --git a/snapshots/ExecuteBatch.json b/snapshots/ExecuteBatch.json new file mode 100644 index 0000000..fd11685 --- /dev/null +++ b/snapshots/ExecuteBatch.json @@ -0,0 +1,7 @@ +{ + "batch_1": "67889", + "batch_2": "94597", + "batch_3": "106529", + "batch_4": "119734", + "batch_5": "132991" +} \ No newline at end of file diff --git a/snapshots/Execute_ETHTransfer_GasBenchmark.json b/snapshots/Execute_ETHTransfer_GasBenchmark.json new file mode 100644 index 0000000..bc2e1c8 --- /dev/null +++ b/snapshots/Execute_ETHTransfer_GasBenchmark.json @@ -0,0 +1,3 @@ +{ + "Execute_ETHTransfer": "66046" +} \ No newline at end of file diff --git a/test/gas/ExecuteGasBenchmark.t.sol b/test/gas/ExecuteGasBenchmark.t.sol new file mode 100644 index 0000000..c41ad5f --- /dev/null +++ b/test/gas/ExecuteGasBenchmark.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {console2} from "forge-std/Test.sol"; +import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol"; +import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; + +/// forge-config: default.isolate = true +contract ExecuteGasBenchmarkBase is SmartWalletTestBase { + // Standard test values + uint256 internal constant BENCHMARK_ETH_AMOUNT = 1 ether; + address internal constant BENCHMARK_RECIPIENT = address(0x1234); + + function setUp() public virtual override { + super.setUp(); + + // Do a dummy transfer to initialize any storage slots + vm.deal(address(account), 10 ether); + vm.prank(signer); + account.execute(address(0x9999), 1 wei, ""); + } +} + +contract Execute_ETHTransfer_GasBenchmark is ExecuteGasBenchmarkBase { + // This will show up in --gas-report + function test_execute_ethTransfer_benchmark() public { + vm.prank(signer); + account.execute(BENCHMARK_RECIPIENT, BENCHMARK_ETH_AMOUNT, ""); + } + + // This will also show up in --gas-report + // and writes to snapshots/Execute_ETHTransfer_GasBenchmark.json + function test_execute_ethTransfer_snapshot() public { + // The snapshot captures ONLY this execute call + vm.prank(signer); + vm.startSnapshotGas("Execute_ETHTransfer"); + account.execute(BENCHMARK_RECIPIENT, BENCHMARK_ETH_AMOUNT, ""); + uint256 gasUsed = vm.stopSnapshotGas(); + + // Optional: log for immediate visibility + console2.log("ETH Transfer gas (snapshot):", gasUsed); + + // Gas report will show the ENTIRE function (including vm.prank, console.log, etc.) + // Snapshot will show ONLY the execute call + } +} + +contract Execute_SelfCall_GasBenchmark is ExecuteGasBenchmarkBase { + function test_execute_selfCall_benchmark() public { + // Measure only overhead without external call + vm.prank(signer); + account.execute(address(account), 0, ""); + } +} + +// Simple target contract for testing +contract Target { + uint256 public value; + function setValue(uint256 v) external { value = v; } +} + +contract Execute_ContractCall_GasBenchmark is ExecuteGasBenchmarkBase { + Target target; + + function setUp() public override { + super.setUp(); + target = new Target(); + } + + function test_execute_contractCall_benchmark() public { + bytes memory data = abi.encodeCall(Target.setValue, (42)); + vm.prank(signer); + account.execute(address(target), 0, data); + } +} + +contract ExecuteBatch_GasBenchmark is ExecuteGasBenchmarkBase { + function test_executeBatch_3transfers_benchmark() public { + CoinbaseSmartWallet.Call[] memory calls = new CoinbaseSmartWallet.Call[](3); + + calls[0] = CoinbaseSmartWallet.Call({ + target: address(0x1111), + value: 0.1 ether, + data: "" + }); + + calls[1] = CoinbaseSmartWallet.Call({ + target: address(0x2222), + value: 0.1 ether, + data: "" + }); + + calls[2] = CoinbaseSmartWallet.Call({ + target: address(0x3333), + value: 0.1 ether, + data: "" + }); + + vm.prank(signer); + account.executeBatch(calls); + } + + function test_executeBatch_detailed_analysis() public { + // Measure batch sizes 1-5 to see scaling + for (uint256 size = 1; size <= 5; size++) { + CoinbaseSmartWallet.Call[] memory calls = new CoinbaseSmartWallet.Call[](size); + + for (uint256 i = 0; i < size; i++) { + calls[i] = CoinbaseSmartWallet.Call({ + target: address(uint160(0x1000 + i)), + value: 0.01 ether, + data: "" + }); + } + + vm.prank(signer); + string memory label = string(abi.encodePacked("batch_", vm.toString(size))); + vm.startSnapshotGas("ExecuteBatch", label); + account.executeBatch(calls); + uint256 gasUsed = vm.stopSnapshotGas(); + + console2.log("Batch size", size, "gas:", gasUsed); + if (size > 1) { + console2.log(" Per call:", gasUsed / size); + } + } + } +} + +contract Execute_ViaEntryPoint_GasBenchmark is ExecuteGasBenchmarkBase { + function test_execute_viaEntryPoint_benchmark() public { + // Test execution when called via EntryPoint (not owner) + vm.prank(account.entryPoint()); + account.execute(BENCHMARK_RECIPIENT, BENCHMARK_ETH_AMOUNT, ""); + } +} + +contract ExecuteWithoutChainIdValidation_GasBenchmark is ExecuteGasBenchmarkBase { + function test_executeWithoutChainIdValidation_benchmark() public { + // Setup call to add owner (allowed cross-chain operation) + bytes memory addOwnerCall = abi.encodeWithSignature( + "addOwnerAddress(address)", + address(0x5555) + ); + + bytes[] memory calls = new bytes[](1); + calls[0] = addOwnerCall; + + vm.prank(account.entryPoint()); + account.executeWithoutChainIdValidation(calls); + } +} \ No newline at end of file From da81d3440860e1b7666065ebf40f24974c32c3c3 Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Thu, 4 Sep 2025 14:38:53 -0700 Subject: [PATCH 2/8] updates --- snapshots/EndToEndTest.json | 11 + snapshots/ExecuteBatch.json | 7 - .../Execute_ETHTransfer_GasBenchmark.json | 3 - snapshots/ValidateUserOpTest.json | 8 + test/gas/EndToEnd.t.sol | 223 ++++++++++++++++++ test/gas/ExecuteGasBenchmark.t.sol | 152 ------------ test/gas/validateUserOp.t.sol | 137 +++++++++++ 7 files changed, 379 insertions(+), 162 deletions(-) create mode 100644 snapshots/EndToEndTest.json delete mode 100644 snapshots/ExecuteBatch.json delete mode 100644 snapshots/Execute_ETHTransfer_GasBenchmark.json create mode 100644 snapshots/ValidateUserOpTest.json create mode 100644 test/gas/EndToEnd.t.sol delete mode 100644 test/gas/ExecuteGasBenchmark.t.sol create mode 100644 test/gas/validateUserOp.t.sol diff --git a/snapshots/EndToEndTest.json b/snapshots/EndToEndTest.json new file mode 100644 index 0000000..0afc30f --- /dev/null +++ b/snapshots/EndToEndTest.json @@ -0,0 +1,11 @@ +{ + "e2e_create_contentcoin_4337": "176243", + "e2e_create_contentcoin_eoa": "45103", + "e2e_swap_eth_contentcoin_4337": "231229", + "e2e_swap_eth_usdc_4337": "182871", + "e2e_swap_eth_usdc_eoa": "51665", + "e2e_transfer_erc20_4337": "159402", + "e2e_transfer_erc20_eoa": "50910", + "e2e_transfer_native_4337": "159635", + "e2e_transfer_native_eoa": "17652" +} \ No newline at end of file diff --git a/snapshots/ExecuteBatch.json b/snapshots/ExecuteBatch.json deleted file mode 100644 index fd11685..0000000 --- a/snapshots/ExecuteBatch.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "batch_1": "67889", - "batch_2": "94597", - "batch_3": "106529", - "batch_4": "119734", - "batch_5": "132991" -} \ No newline at end of file diff --git a/snapshots/Execute_ETHTransfer_GasBenchmark.json b/snapshots/Execute_ETHTransfer_GasBenchmark.json deleted file mode 100644 index bc2e1c8..0000000 --- a/snapshots/Execute_ETHTransfer_GasBenchmark.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "Execute_ETHTransfer": "66046" -} \ No newline at end of file diff --git a/snapshots/ValidateUserOpTest.json b/snapshots/ValidateUserOpTest.json new file mode 100644 index 0000000..595f435 --- /dev/null +++ b/snapshots/ValidateUserOpTest.json @@ -0,0 +1,8 @@ +{ + "validation_k1": "63813", + "validation_k1_replayable": "71155", + "validation_r1_7212": "78367", + "validation_r1_7212_replayable": "85392", + "validation_r1_FCL": "78370", + "validation_r1_FCL_replayable": "85395" +} \ No newline at end of file diff --git a/test/gas/EndToEnd.t.sol b/test/gas/EndToEnd.t.sol new file mode 100644 index 0000000..22553ac --- /dev/null +++ b/test/gas/EndToEnd.t.sol @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {console2} from "forge-std/Test.sol"; +import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; +import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol"; +import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol"; +import {MockTarget} from "../mocks/MockTarget.sol"; +import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol"; +import {MockERC20} from "../../lib/solady/test/utils/mocks/MockERC20.sol"; +import {Static} from "../CoinbaseSmartWallet/Static.sol"; + +/// forge-config: default.isolate = true +contract EndToEndTest is SmartWalletTestBase { + // EOA baseline comparison + address eoaUser = address(0xe0a); + + MockERC20 usdc; + MockTarget target; + CoinbaseSmartWalletFactory factory; + + function setUp() public override { + // Set up EntryPoint + vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES); + + // Deploy factory and create account + CoinbaseSmartWallet implementation = new CoinbaseSmartWallet(); + factory = new CoinbaseSmartWalletFactory(address(implementation)); + + // Create account with the factory + signerPrivateKey = 0xa11ce; + signer = vm.addr(signerPrivateKey); + owners.push(abi.encode(signer)); + account = factory.createAccount(owners, 0); + + // Fund accounts + vm.deal(address(account), 100 ether); + vm.deal(eoaUser, 100 ether); + + // Deploy mocks + usdc = new MockERC20("USD Coin", "USDC", 6); + usdc.mint(address(account), 10000e6); + usdc.mint(eoaUser, 10000e6); + + target = new MockTarget(); + } + + function test_transfer_native() public { + // Prepare UserOp + userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.execute, (address(0x1234), 1 ether, "")); + UserOperation memory op = _getUserOpWithSignature(); + + // Log calldata + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); + console2.log("test_transfer_native ERC-4337 calldata size:", handleOpsCalldata.length); + + // Execute via EntryPoint + vm.startSnapshotGas("e2e_transfer_native_4337"); + _sendUserOperation(op); + uint256 gas4337 = vm.stopSnapshotGas(); + console2.log("test_transfer_native ERC-4337 gas:", gas4337); + + // EOA comparison + console2.log("test_transfer_native EOA calldata size:", uint256(0)); // ETH transfers have no calldata + + vm.prank(eoaUser); + vm.startSnapshotGas("e2e_transfer_native_eoa"); + payable(address(0x1234)).transfer(1 ether); + uint256 gasEOA = vm.stopSnapshotGas(); + console2.log("test_transfer_native EOA gas:", gasEOA); + console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); + } + + function test_transfer_erc20() public { + // Prepare UserOp + userOpCalldata = abi.encodeCall( + CoinbaseSmartWallet.execute, + (address(usdc), 0, abi.encodeCall(usdc.transfer, (address(0x5678), 100e6))) + ); + UserOperation memory op = _getUserOpWithSignature(); + + // Log calldata + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); + console2.log("test_transfer_erc20 ERC-4337 calldata size:", handleOpsCalldata.length); + + // Execute via EntryPoint + vm.startSnapshotGas("e2e_transfer_erc20_4337"); + _sendUserOperation(op); + uint256 gas4337 = vm.stopSnapshotGas(); + console2.log("test_transfer_erc20 ERC-4337 gas:", gas4337); + + // EOA comparison + bytes memory eoaCalldata = abi.encodeCall(usdc.transfer, (address(0x5678), 100e6)); + console2.log("test_transfer_erc20 EOA calldata size:", eoaCalldata.length); + + vm.prank(eoaUser); + vm.startSnapshotGas("e2e_transfer_erc20_eoa"); + usdc.transfer(address(0x5678), 100e6); + uint256 gasEOA = vm.stopSnapshotGas(); + console2.log("test_transfer_erc20 EOA gas:", gasEOA); + console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); + } + + function test_swap_eth_usdc_uniV4() public { + // Mock swap: send ETH to target, get USDC back + userOpCalldata = abi.encodeCall( + CoinbaseSmartWallet.execute, + (address(target), 0.1 ether, abi.encodeCall(target.setData, ("swap_eth_usdc"))) + ); + UserOperation memory op = _getUserOpWithSignature(); + + // Log calldata + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); + console2.log("test_swap_eth_usdc_uniV4 ERC-4337 calldata size:", handleOpsCalldata.length); + + // Execute via EntryPoint + vm.startSnapshotGas("e2e_swap_eth_usdc_4337"); + _sendUserOperation(op); + uint256 gas4337 = vm.stopSnapshotGas(); + console2.log("test_swap_eth_usdc_uniV4 ERC-4337 gas:", gas4337); + + // EOA comparison + bytes memory eoaCalldata = abi.encodeCall(target.setData, ("swap_eth_usdc")); + console2.log("test_swap_eth_usdc_uniV4 EOA calldata size:", eoaCalldata.length); + + vm.prank(eoaUser); + vm.startSnapshotGas("e2e_swap_eth_usdc_eoa"); + target.setData{value: 0.1 ether}("swap_eth_usdc"); + uint256 gasEOA = vm.stopSnapshotGas(); + console2.log("test_swap_eth_usdc_uniV4 EOA gas:", gasEOA); + console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); + } + + function test_swap_eth_contentcoin_uniV4() public { + // Multi-hop swap simulation using executeBatch + CoinbaseSmartWallet.Call[] memory calls = new CoinbaseSmartWallet.Call[](4); + + calls[0] = CoinbaseSmartWallet.Call({ + target: address(target), + value: 0.1 ether, + data: abi.encodeCall(target.setData, ("swap_eth_usdc")) + }); + + calls[1] = CoinbaseSmartWallet.Call({ + target: address(usdc), + value: 0, + data: abi.encodeCall(usdc.approve, (address(target), 100e6)) + }); + + calls[2] = CoinbaseSmartWallet.Call({ + target: address(target), + value: 0, + data: abi.encodeCall(target.setData, ("swap_usdc_zora")) + }); + + calls[3] = CoinbaseSmartWallet.Call({ + target: address(target), + value: 0, + data: abi.encodeCall(target.setData, ("swap_zora_contentcoin")) + }); + + userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.executeBatch, (calls)); + UserOperation memory op = _getUserOpWithSignature(); + + // Log calldata + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); + console2.log("test_swap_eth_contentcoin_uniV4 ERC-4337 calldata size:", handleOpsCalldata.length); + + // Execute via EntryPoint + vm.startSnapshotGas("e2e_swap_eth_contentcoin_4337"); + _sendUserOperation(op); + uint256 gas4337 = vm.stopSnapshotGas(); + console2.log("test_swap_eth_contentcoin_uniV4 ERC-4337 gas:", gas4337); + + // EOA would require 4 separate transactions + console2.log("test_swap_eth_contentcoin_uniV4 EOA calldata size: N/A (4 separate txs)"); + console2.log("test_swap_eth_contentcoin_uniV4 EOA gas: N/A (4 separate txs required)"); + } + + function test_create_contentcoin() public { + // Simulate content coin creation + userOpCalldata = abi.encodeCall( + CoinbaseSmartWallet.execute, + (address(target), 0, abi.encodeCall(target.setData, ("create_contentcoin_MyCoin"))) + ); + UserOperation memory op = _getUserOpWithSignature(); + + // Log calldata + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); + console2.log("test_create_contentcoin ERC-4337 calldata size:", handleOpsCalldata.length); + + // Execute via EntryPoint + vm.startSnapshotGas("e2e_create_contentcoin_4337"); + _sendUserOperation(op); + uint256 gas4337 = vm.stopSnapshotGas(); + console2.log("test_create_contentcoin ERC-4337 gas:", gas4337); + + // EOA comparison + bytes memory eoaCalldata = abi.encodeCall(target.setData, ("create_contentcoin_MyCoin")); + console2.log("test_create_contentcoin EOA calldata size:", eoaCalldata.length); + + vm.prank(eoaUser); + vm.startSnapshotGas("e2e_create_contentcoin_eoa"); + target.setData("create_contentcoin_MyCoin"); + uint256 gasEOA = vm.stopSnapshotGas(); + console2.log("test_create_contentcoin EOA gas:", gasEOA); + console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); + } + + // Helper to create UserOperation array + function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) { + UserOperation[] memory ops = new UserOperation[](1); + ops[0] = op; + return ops; + } + + // Override signature generation to use correct format for CoinbaseSmartWallet + 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))); + } +} \ No newline at end of file diff --git a/test/gas/ExecuteGasBenchmark.t.sol b/test/gas/ExecuteGasBenchmark.t.sol deleted file mode 100644 index c41ad5f..0000000 --- a/test/gas/ExecuteGasBenchmark.t.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {console2} from "forge-std/Test.sol"; -import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol"; -import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; - -/// forge-config: default.isolate = true -contract ExecuteGasBenchmarkBase is SmartWalletTestBase { - // Standard test values - uint256 internal constant BENCHMARK_ETH_AMOUNT = 1 ether; - address internal constant BENCHMARK_RECIPIENT = address(0x1234); - - function setUp() public virtual override { - super.setUp(); - - // Do a dummy transfer to initialize any storage slots - vm.deal(address(account), 10 ether); - vm.prank(signer); - account.execute(address(0x9999), 1 wei, ""); - } -} - -contract Execute_ETHTransfer_GasBenchmark is ExecuteGasBenchmarkBase { - // This will show up in --gas-report - function test_execute_ethTransfer_benchmark() public { - vm.prank(signer); - account.execute(BENCHMARK_RECIPIENT, BENCHMARK_ETH_AMOUNT, ""); - } - - // This will also show up in --gas-report - // and writes to snapshots/Execute_ETHTransfer_GasBenchmark.json - function test_execute_ethTransfer_snapshot() public { - // The snapshot captures ONLY this execute call - vm.prank(signer); - vm.startSnapshotGas("Execute_ETHTransfer"); - account.execute(BENCHMARK_RECIPIENT, BENCHMARK_ETH_AMOUNT, ""); - uint256 gasUsed = vm.stopSnapshotGas(); - - // Optional: log for immediate visibility - console2.log("ETH Transfer gas (snapshot):", gasUsed); - - // Gas report will show the ENTIRE function (including vm.prank, console.log, etc.) - // Snapshot will show ONLY the execute call - } -} - -contract Execute_SelfCall_GasBenchmark is ExecuteGasBenchmarkBase { - function test_execute_selfCall_benchmark() public { - // Measure only overhead without external call - vm.prank(signer); - account.execute(address(account), 0, ""); - } -} - -// Simple target contract for testing -contract Target { - uint256 public value; - function setValue(uint256 v) external { value = v; } -} - -contract Execute_ContractCall_GasBenchmark is ExecuteGasBenchmarkBase { - Target target; - - function setUp() public override { - super.setUp(); - target = new Target(); - } - - function test_execute_contractCall_benchmark() public { - bytes memory data = abi.encodeCall(Target.setValue, (42)); - vm.prank(signer); - account.execute(address(target), 0, data); - } -} - -contract ExecuteBatch_GasBenchmark is ExecuteGasBenchmarkBase { - function test_executeBatch_3transfers_benchmark() public { - CoinbaseSmartWallet.Call[] memory calls = new CoinbaseSmartWallet.Call[](3); - - calls[0] = CoinbaseSmartWallet.Call({ - target: address(0x1111), - value: 0.1 ether, - data: "" - }); - - calls[1] = CoinbaseSmartWallet.Call({ - target: address(0x2222), - value: 0.1 ether, - data: "" - }); - - calls[2] = CoinbaseSmartWallet.Call({ - target: address(0x3333), - value: 0.1 ether, - data: "" - }); - - vm.prank(signer); - account.executeBatch(calls); - } - - function test_executeBatch_detailed_analysis() public { - // Measure batch sizes 1-5 to see scaling - for (uint256 size = 1; size <= 5; size++) { - CoinbaseSmartWallet.Call[] memory calls = new CoinbaseSmartWallet.Call[](size); - - for (uint256 i = 0; i < size; i++) { - calls[i] = CoinbaseSmartWallet.Call({ - target: address(uint160(0x1000 + i)), - value: 0.01 ether, - data: "" - }); - } - - vm.prank(signer); - string memory label = string(abi.encodePacked("batch_", vm.toString(size))); - vm.startSnapshotGas("ExecuteBatch", label); - account.executeBatch(calls); - uint256 gasUsed = vm.stopSnapshotGas(); - - console2.log("Batch size", size, "gas:", gasUsed); - if (size > 1) { - console2.log(" Per call:", gasUsed / size); - } - } - } -} - -contract Execute_ViaEntryPoint_GasBenchmark is ExecuteGasBenchmarkBase { - function test_execute_viaEntryPoint_benchmark() public { - // Test execution when called via EntryPoint (not owner) - vm.prank(account.entryPoint()); - account.execute(BENCHMARK_RECIPIENT, BENCHMARK_ETH_AMOUNT, ""); - } -} - -contract ExecuteWithoutChainIdValidation_GasBenchmark is ExecuteGasBenchmarkBase { - function test_executeWithoutChainIdValidation_benchmark() public { - // Setup call to add owner (allowed cross-chain operation) - bytes memory addOwnerCall = abi.encodeWithSignature( - "addOwnerAddress(address)", - address(0x5555) - ); - - bytes[] memory calls = new bytes[](1); - calls[0] = addOwnerCall; - - vm.prank(account.entryPoint()); - account.executeWithoutChainIdValidation(calls); - } -} \ No newline at end of file diff --git a/test/gas/validateUserOp.t.sol b/test/gas/validateUserOp.t.sol new file mode 100644 index 0000000..f73e723 --- /dev/null +++ b/test/gas/validateUserOp.t.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {console2} from "forge-std/Test.sol"; +import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; +import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol"; +import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol"; +import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol"; +import {WebAuthn} from "webauthn-sol/WebAuthn.sol"; + +/// forge-config: default.isolate = true +contract ValidateUserOpTest is SmartWalletTestBase { + bytes32 constant TEST_HASH = keccak256("test operation"); + CoinbaseSmartWalletFactory factory; + + function setUp() public override { + super.setUp(); + vm.etch(account.entryPoint(), address(new MockEntryPoint()).code); + + // Deploy factory + CoinbaseSmartWallet implementation = new CoinbaseSmartWallet(); + factory = new CoinbaseSmartWalletFactory(address(implementation)); + } + + function test_k1() public { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, TEST_HASH); + UserOperation memory op; + op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v))); + + vm.startSnapshotGas("validation_k1"); + MockEntryPoint(account.entryPoint()).validateUserOp(address(account), op, TEST_HASH, 0); + uint256 gasUsed = vm.stopSnapshotGas(); + console2.log("test_k1 gas:", gasUsed); + } + + function test_r1_7212() public { + // Simulate EIP-7212 precompile support + bytes[] memory owners = new bytes[](1); + owners[0] = passkeyOwner; + CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); + + WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); + UserOperation memory op; + op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); + + // Mock 7212 precompile exists + vm.etch(address(0x0100), hex"60FF"); // Simple return true + + vm.startSnapshotGas("validation_r1_7212"); + MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); + uint256 gasUsed = vm.stopSnapshotGas(); + console2.log("test_r1_7212 gas:", gasUsed); + } + + function test_r1_FCL() public { + // Without 7212 precompile - uses FCL library + bytes[] memory owners = new bytes[](1); + owners[0] = passkeyOwner; + CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); + + WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); + UserOperation memory op; + op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); + + vm.startSnapshotGas("validation_r1_FCL"); + MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); + uint256 gasUsed = vm.stopSnapshotGas(); + console2.log("test_r1_FCL gas:", gasUsed); + } + + function test_k1_replayable() public { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, TEST_HASH); + UserOperation memory op; + op.nonce = account.REPLAYABLE_NONCE_KEY() << 64; + op.callData = abi.encodeCall(CoinbaseSmartWallet.executeWithoutChainIdValidation, (new bytes[](1))); + op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v))); + + vm.startSnapshotGas("validation_k1_replayable"); + MockEntryPoint(account.entryPoint()).validateUserOp(address(account), op, TEST_HASH, 0); + uint256 gasUsed = vm.stopSnapshotGas(); + console2.log("test_k1_replayable gas:", gasUsed); + } + + function test_r1_7212_replayable() public { + bytes[] memory owners = new bytes[](1); + owners[0] = passkeyOwner; + CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); + + WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); + UserOperation memory op; + op.nonce = passkeyAccount.REPLAYABLE_NONCE_KEY() << 64; + op.callData = abi.encodeCall(CoinbaseSmartWallet.executeWithoutChainIdValidation, (new bytes[](1))); + op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); + + vm.etch(address(0x0100), hex"60FF"); // Mock 7212 + + vm.startSnapshotGas("validation_r1_7212_replayable"); + MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); + uint256 gasUsed = vm.stopSnapshotGas(); + console2.log("test_r1_7212_replayable gas:", gasUsed); + } + + function test_r1_FCL_replayable() public { + bytes[] memory owners = new bytes[](1); + owners[0] = passkeyOwner; + CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); + + WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); + UserOperation memory op; + op.nonce = passkeyAccount.REPLAYABLE_NONCE_KEY() << 64; + op.callData = abi.encodeCall(CoinbaseSmartWallet.executeWithoutChainIdValidation, (new bytes[](1))); + op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); + + vm.startSnapshotGas("validation_r1_FCL_replayable"); + MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); + uint256 gasUsed = vm.stopSnapshotGas(); + console2.log("test_r1_FCL_replayable gas:", gasUsed); + } + + function createWebAuthnAuth() internal pure returns (WebAuthn.WebAuthnAuth memory) { + return WebAuthn.WebAuthnAuth({ + authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000", + clientDataJSON: '{"type":"webauthn.get","challenge":"', + challengeIndex: 23, + typeIndex: 1, + r: 0x7e7de1a8b53f9fea2e6b2b8f0e564c126a5b3a8f0e1234567890abcdef123456, + s: 0x3b2a5c8f5e4d6c7b8a9b0c1d2e3f4051627384950617283940516273849506ff + }); + } +} + +contract MockEntryPoint { + function validateUserOp(address account, UserOperation memory op, bytes32 hash, uint256 funds) + external returns (uint256) { + return CoinbaseSmartWallet(payable(account)).validateUserOp(op, hash, funds); + } +} \ No newline at end of file From 9ba2ff1fdf5013b8c4a2679aa5819d5b9c62d108 Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Thu, 4 Sep 2025 15:11:37 -0700 Subject: [PATCH 3/8] fixes --- test/gas/EndToEnd.t.sol | 36 +++++++---------------------------- test/gas/validateUserOp.t.sol | 17 ++++++++++------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/test/gas/EndToEnd.t.sol b/test/gas/EndToEnd.t.sol index 22553ac..86c4693 100644 --- a/test/gas/EndToEnd.t.sol +++ b/test/gas/EndToEnd.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; +pragma solidity ^0.8.4; import {console2} from "forge-std/Test.sol"; import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; @@ -12,7 +12,6 @@ import {Static} from "../CoinbaseSmartWallet/Static.sol"; /// forge-config: default.isolate = true contract EndToEndTest is SmartWalletTestBase { - // EOA baseline comparison address eoaUser = address(0xe0a); MockERC20 usdc; @@ -20,24 +19,19 @@ contract EndToEndTest is SmartWalletTestBase { CoinbaseSmartWalletFactory factory; function setUp() public override { - // Set up EntryPoint vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES); - // Deploy factory and create account CoinbaseSmartWallet implementation = new CoinbaseSmartWallet(); factory = new CoinbaseSmartWalletFactory(address(implementation)); - // Create account with the factory signerPrivateKey = 0xa11ce; signer = vm.addr(signerPrivateKey); owners.push(abi.encode(signer)); account = factory.createAccount(owners, 0); - // Fund accounts vm.deal(address(account), 100 ether); vm.deal(eoaUser, 100 ether); - // Deploy mocks usdc = new MockERC20("USD Coin", "USDC", 6); usdc.mint(address(account), 10000e6); usdc.mint(eoaUser, 10000e6); @@ -45,23 +39,20 @@ contract EndToEndTest is SmartWalletTestBase { target = new MockTarget(); } + // Native ETH transfer comparison between ERC-4337 and EOA function test_transfer_native() public { - // Prepare UserOp userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.execute, (address(0x1234), 1 ether, "")); UserOperation memory op = _getUserOpWithSignature(); - // Log calldata bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); console2.log("test_transfer_native ERC-4337 calldata size:", handleOpsCalldata.length); - // Execute via EntryPoint vm.startSnapshotGas("e2e_transfer_native_4337"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); console2.log("test_transfer_native ERC-4337 gas:", gas4337); - // EOA comparison - console2.log("test_transfer_native EOA calldata size:", uint256(0)); // ETH transfers have no calldata + console2.log("test_transfer_native EOA calldata size:", uint256(0)); vm.prank(eoaUser); vm.startSnapshotGas("e2e_transfer_native_eoa"); @@ -71,25 +62,22 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); } + // ERC20 transfer comparison showing overhead for token operations function test_transfer_erc20() public { - // Prepare UserOp userOpCalldata = abi.encodeCall( CoinbaseSmartWallet.execute, (address(usdc), 0, abi.encodeCall(usdc.transfer, (address(0x5678), 100e6))) ); UserOperation memory op = _getUserOpWithSignature(); - // Log calldata bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); console2.log("test_transfer_erc20 ERC-4337 calldata size:", handleOpsCalldata.length); - // Execute via EntryPoint vm.startSnapshotGas("e2e_transfer_erc20_4337"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); console2.log("test_transfer_erc20 ERC-4337 gas:", gas4337); - // EOA comparison bytes memory eoaCalldata = abi.encodeCall(usdc.transfer, (address(0x5678), 100e6)); console2.log("test_transfer_erc20 EOA calldata size:", eoaCalldata.length); @@ -101,25 +89,22 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); } + // Simple swap simulation - single contract interaction with ETH value function test_swap_eth_usdc_uniV4() public { - // Mock swap: send ETH to target, get USDC back userOpCalldata = abi.encodeCall( CoinbaseSmartWallet.execute, (address(target), 0.1 ether, abi.encodeCall(target.setData, ("swap_eth_usdc"))) ); UserOperation memory op = _getUserOpWithSignature(); - // Log calldata bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); console2.log("test_swap_eth_usdc_uniV4 ERC-4337 calldata size:", handleOpsCalldata.length); - // Execute via EntryPoint vm.startSnapshotGas("e2e_swap_eth_usdc_4337"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); console2.log("test_swap_eth_usdc_uniV4 ERC-4337 gas:", gas4337); - // EOA comparison bytes memory eoaCalldata = abi.encodeCall(target.setData, ("swap_eth_usdc")); console2.log("test_swap_eth_usdc_uniV4 EOA calldata size:", eoaCalldata.length); @@ -131,8 +116,8 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); } + // Multi-hop swap demonstrating batch execution advantages function test_swap_eth_contentcoin_uniV4() public { - // Multi-hop swap simulation using executeBatch CoinbaseSmartWallet.Call[] memory calls = new CoinbaseSmartWallet.Call[](4); calls[0] = CoinbaseSmartWallet.Call({ @@ -162,11 +147,9 @@ contract EndToEndTest is SmartWalletTestBase { userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.executeBatch, (calls)); UserOperation memory op = _getUserOpWithSignature(); - // Log calldata bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); console2.log("test_swap_eth_contentcoin_uniV4 ERC-4337 calldata size:", handleOpsCalldata.length); - // Execute via EntryPoint vm.startSnapshotGas("e2e_swap_eth_contentcoin_4337"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); @@ -177,25 +160,22 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("test_swap_eth_contentcoin_uniV4 EOA gas: N/A (4 separate txs required)"); } + // Contract creation simulation to measure deployment overhead function test_create_contentcoin() public { - // Simulate content coin creation userOpCalldata = abi.encodeCall( CoinbaseSmartWallet.execute, (address(target), 0, abi.encodeCall(target.setData, ("create_contentcoin_MyCoin"))) ); UserOperation memory op = _getUserOpWithSignature(); - // Log calldata bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); console2.log("test_create_contentcoin ERC-4337 calldata size:", handleOpsCalldata.length); - // Execute via EntryPoint vm.startSnapshotGas("e2e_create_contentcoin_4337"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); console2.log("test_create_contentcoin ERC-4337 gas:", gas4337); - // EOA comparison bytes memory eoaCalldata = abi.encodeCall(target.setData, ("create_contentcoin_MyCoin")); console2.log("test_create_contentcoin EOA calldata size:", eoaCalldata.length); @@ -207,14 +187,12 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); } - // Helper to create UserOperation array function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) { UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; return ops; } - // Override signature generation to use correct format for CoinbaseSmartWallet 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); diff --git a/test/gas/validateUserOp.t.sol b/test/gas/validateUserOp.t.sol index f73e723..799280b 100644 --- a/test/gas/validateUserOp.t.sol +++ b/test/gas/validateUserOp.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; +pragma solidity ^0.8.4; import {console2} from "forge-std/Test.sol"; import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; @@ -17,11 +17,11 @@ contract ValidateUserOpTest is SmartWalletTestBase { super.setUp(); vm.etch(account.entryPoint(), address(new MockEntryPoint()).code); - // Deploy factory CoinbaseSmartWallet implementation = new CoinbaseSmartWallet(); factory = new CoinbaseSmartWalletFactory(address(implementation)); } + // Standard K1 (ECDSA) signature validation function test_k1() public { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, TEST_HASH); UserOperation memory op; @@ -33,8 +33,8 @@ contract ValidateUserOpTest is SmartWalletTestBase { console2.log("test_k1 gas:", gasUsed); } + // R1 (passkey) validation with simulated EIP-7212 precompile function test_r1_7212() public { - // Simulate EIP-7212 precompile support bytes[] memory owners = new bytes[](1); owners[0] = passkeyOwner; CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); @@ -43,8 +43,8 @@ contract ValidateUserOpTest is SmartWalletTestBase { UserOperation memory op; op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); - // Mock 7212 precompile exists - vm.etch(address(0x0100), hex"60FF"); // Simple return true + // Mock 7212 precompile at 0x0100 + vm.etch(address(0x0100), hex"60FF"); vm.startSnapshotGas("validation_r1_7212"); MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); @@ -52,8 +52,8 @@ contract ValidateUserOpTest is SmartWalletTestBase { console2.log("test_r1_7212 gas:", gasUsed); } + // R1 validation using FCL library (current implementation) function test_r1_FCL() public { - // Without 7212 precompile - uses FCL library bytes[] memory owners = new bytes[](1); owners[0] = passkeyOwner; CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); @@ -68,6 +68,7 @@ contract ValidateUserOpTest is SmartWalletTestBase { console2.log("test_r1_FCL gas:", gasUsed); } + // K1 validation with cross-chain replayable nonce function test_k1_replayable() public { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, TEST_HASH); UserOperation memory op; @@ -81,6 +82,7 @@ contract ValidateUserOpTest is SmartWalletTestBase { console2.log("test_k1_replayable gas:", gasUsed); } + // R1 validation with 7212 precompile and cross-chain replayable nonce function test_r1_7212_replayable() public { bytes[] memory owners = new bytes[](1); owners[0] = passkeyOwner; @@ -92,7 +94,7 @@ contract ValidateUserOpTest is SmartWalletTestBase { op.callData = abi.encodeCall(CoinbaseSmartWallet.executeWithoutChainIdValidation, (new bytes[](1))); op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); - vm.etch(address(0x0100), hex"60FF"); // Mock 7212 + vm.etch(address(0x0100), hex"60FF"); vm.startSnapshotGas("validation_r1_7212_replayable"); MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); @@ -100,6 +102,7 @@ contract ValidateUserOpTest is SmartWalletTestBase { console2.log("test_r1_7212_replayable gas:", gasUsed); } + // R1 validation with FCL and cross-chain replayable nonce function test_r1_FCL_replayable() public { bytes[] memory owners = new bytes[](1); owners[0] = passkeyOwner; From 1e99582277d79f32089264f0d559d0e817b5f83b Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Fri, 5 Sep 2025 10:39:44 -0700 Subject: [PATCH 4/8] erc20 and native transfer --- snapshots/EndToEndTest.json | 5 -- snapshots/ValidateUserOpTest.json | 8 -- test/gas/EndToEnd.t.sol | 100 +-------------------- test/gas/validateUserOp.t.sol | 140 ------------------------------ 4 files changed, 1 insertion(+), 252 deletions(-) delete mode 100644 snapshots/ValidateUserOpTest.json delete mode 100644 test/gas/validateUserOp.t.sol diff --git a/snapshots/EndToEndTest.json b/snapshots/EndToEndTest.json index 0afc30f..3eed19d 100644 --- a/snapshots/EndToEndTest.json +++ b/snapshots/EndToEndTest.json @@ -1,9 +1,4 @@ { - "e2e_create_contentcoin_4337": "176243", - "e2e_create_contentcoin_eoa": "45103", - "e2e_swap_eth_contentcoin_4337": "231229", - "e2e_swap_eth_usdc_4337": "182871", - "e2e_swap_eth_usdc_eoa": "51665", "e2e_transfer_erc20_4337": "159402", "e2e_transfer_erc20_eoa": "50910", "e2e_transfer_native_4337": "159635", diff --git a/snapshots/ValidateUserOpTest.json b/snapshots/ValidateUserOpTest.json deleted file mode 100644 index 595f435..0000000 --- a/snapshots/ValidateUserOpTest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "validation_k1": "63813", - "validation_k1_replayable": "71155", - "validation_r1_7212": "78367", - "validation_r1_7212_replayable": "85392", - "validation_r1_FCL": "78370", - "validation_r1_FCL_replayable": "85395" -} \ No newline at end of file diff --git a/test/gas/EndToEnd.t.sol b/test/gas/EndToEnd.t.sol index 86c4693..c5a6fc7 100644 --- a/test/gas/EndToEnd.t.sol +++ b/test/gas/EndToEnd.t.sol @@ -62,7 +62,7 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); } - // ERC20 transfer comparison showing overhead for token operations + // ERC20 transfer comparison between ERC-4337 and EOA function test_transfer_erc20() public { userOpCalldata = abi.encodeCall( CoinbaseSmartWallet.execute, @@ -89,104 +89,6 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); } - // Simple swap simulation - single contract interaction with ETH value - function test_swap_eth_usdc_uniV4() public { - userOpCalldata = abi.encodeCall( - CoinbaseSmartWallet.execute, - (address(target), 0.1 ether, abi.encodeCall(target.setData, ("swap_eth_usdc"))) - ); - UserOperation memory op = _getUserOpWithSignature(); - - bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); - console2.log("test_swap_eth_usdc_uniV4 ERC-4337 calldata size:", handleOpsCalldata.length); - - vm.startSnapshotGas("e2e_swap_eth_usdc_4337"); - _sendUserOperation(op); - uint256 gas4337 = vm.stopSnapshotGas(); - console2.log("test_swap_eth_usdc_uniV4 ERC-4337 gas:", gas4337); - - bytes memory eoaCalldata = abi.encodeCall(target.setData, ("swap_eth_usdc")); - console2.log("test_swap_eth_usdc_uniV4 EOA calldata size:", eoaCalldata.length); - - vm.prank(eoaUser); - vm.startSnapshotGas("e2e_swap_eth_usdc_eoa"); - target.setData{value: 0.1 ether}("swap_eth_usdc"); - uint256 gasEOA = vm.stopSnapshotGas(); - console2.log("test_swap_eth_usdc_uniV4 EOA gas:", gasEOA); - console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); - } - - // Multi-hop swap demonstrating batch execution advantages - function test_swap_eth_contentcoin_uniV4() public { - CoinbaseSmartWallet.Call[] memory calls = new CoinbaseSmartWallet.Call[](4); - - calls[0] = CoinbaseSmartWallet.Call({ - target: address(target), - value: 0.1 ether, - data: abi.encodeCall(target.setData, ("swap_eth_usdc")) - }); - - calls[1] = CoinbaseSmartWallet.Call({ - target: address(usdc), - value: 0, - data: abi.encodeCall(usdc.approve, (address(target), 100e6)) - }); - - calls[2] = CoinbaseSmartWallet.Call({ - target: address(target), - value: 0, - data: abi.encodeCall(target.setData, ("swap_usdc_zora")) - }); - - calls[3] = CoinbaseSmartWallet.Call({ - target: address(target), - value: 0, - data: abi.encodeCall(target.setData, ("swap_zora_contentcoin")) - }); - - userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.executeBatch, (calls)); - UserOperation memory op = _getUserOpWithSignature(); - - bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); - console2.log("test_swap_eth_contentcoin_uniV4 ERC-4337 calldata size:", handleOpsCalldata.length); - - vm.startSnapshotGas("e2e_swap_eth_contentcoin_4337"); - _sendUserOperation(op); - uint256 gas4337 = vm.stopSnapshotGas(); - console2.log("test_swap_eth_contentcoin_uniV4 ERC-4337 gas:", gas4337); - - // EOA would require 4 separate transactions - console2.log("test_swap_eth_contentcoin_uniV4 EOA calldata size: N/A (4 separate txs)"); - console2.log("test_swap_eth_contentcoin_uniV4 EOA gas: N/A (4 separate txs required)"); - } - - // Contract creation simulation to measure deployment overhead - function test_create_contentcoin() public { - userOpCalldata = abi.encodeCall( - CoinbaseSmartWallet.execute, - (address(target), 0, abi.encodeCall(target.setData, ("create_contentcoin_MyCoin"))) - ); - UserOperation memory op = _getUserOpWithSignature(); - - bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); - console2.log("test_create_contentcoin ERC-4337 calldata size:", handleOpsCalldata.length); - - vm.startSnapshotGas("e2e_create_contentcoin_4337"); - _sendUserOperation(op); - uint256 gas4337 = vm.stopSnapshotGas(); - console2.log("test_create_contentcoin ERC-4337 gas:", gas4337); - - bytes memory eoaCalldata = abi.encodeCall(target.setData, ("create_contentcoin_MyCoin")); - console2.log("test_create_contentcoin EOA calldata size:", eoaCalldata.length); - - vm.prank(eoaUser); - vm.startSnapshotGas("e2e_create_contentcoin_eoa"); - target.setData("create_contentcoin_MyCoin"); - uint256 gasEOA = vm.stopSnapshotGas(); - console2.log("test_create_contentcoin EOA gas:", gasEOA); - console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); - } - function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) { UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; diff --git a/test/gas/validateUserOp.t.sol b/test/gas/validateUserOp.t.sol deleted file mode 100644 index 799280b..0000000 --- a/test/gas/validateUserOp.t.sol +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import {console2} from "forge-std/Test.sol"; -import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; -import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol"; -import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol"; -import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol"; -import {WebAuthn} from "webauthn-sol/WebAuthn.sol"; - -/// forge-config: default.isolate = true -contract ValidateUserOpTest is SmartWalletTestBase { - bytes32 constant TEST_HASH = keccak256("test operation"); - CoinbaseSmartWalletFactory factory; - - function setUp() public override { - super.setUp(); - vm.etch(account.entryPoint(), address(new MockEntryPoint()).code); - - CoinbaseSmartWallet implementation = new CoinbaseSmartWallet(); - factory = new CoinbaseSmartWalletFactory(address(implementation)); - } - - // Standard K1 (ECDSA) signature validation - function test_k1() public { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, TEST_HASH); - UserOperation memory op; - op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v))); - - vm.startSnapshotGas("validation_k1"); - MockEntryPoint(account.entryPoint()).validateUserOp(address(account), op, TEST_HASH, 0); - uint256 gasUsed = vm.stopSnapshotGas(); - console2.log("test_k1 gas:", gasUsed); - } - - // R1 (passkey) validation with simulated EIP-7212 precompile - function test_r1_7212() public { - bytes[] memory owners = new bytes[](1); - owners[0] = passkeyOwner; - CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); - - WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); - UserOperation memory op; - op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); - - // Mock 7212 precompile at 0x0100 - vm.etch(address(0x0100), hex"60FF"); - - vm.startSnapshotGas("validation_r1_7212"); - MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); - uint256 gasUsed = vm.stopSnapshotGas(); - console2.log("test_r1_7212 gas:", gasUsed); - } - - // R1 validation using FCL library (current implementation) - function test_r1_FCL() public { - bytes[] memory owners = new bytes[](1); - owners[0] = passkeyOwner; - CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); - - WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); - UserOperation memory op; - op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); - - vm.startSnapshotGas("validation_r1_FCL"); - MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); - uint256 gasUsed = vm.stopSnapshotGas(); - console2.log("test_r1_FCL gas:", gasUsed); - } - - // K1 validation with cross-chain replayable nonce - function test_k1_replayable() public { - (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, TEST_HASH); - UserOperation memory op; - op.nonce = account.REPLAYABLE_NONCE_KEY() << 64; - op.callData = abi.encodeCall(CoinbaseSmartWallet.executeWithoutChainIdValidation, (new bytes[](1))); - op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v))); - - vm.startSnapshotGas("validation_k1_replayable"); - MockEntryPoint(account.entryPoint()).validateUserOp(address(account), op, TEST_HASH, 0); - uint256 gasUsed = vm.stopSnapshotGas(); - console2.log("test_k1_replayable gas:", gasUsed); - } - - // R1 validation with 7212 precompile and cross-chain replayable nonce - function test_r1_7212_replayable() public { - bytes[] memory owners = new bytes[](1); - owners[0] = passkeyOwner; - CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); - - WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); - UserOperation memory op; - op.nonce = passkeyAccount.REPLAYABLE_NONCE_KEY() << 64; - op.callData = abi.encodeCall(CoinbaseSmartWallet.executeWithoutChainIdValidation, (new bytes[](1))); - op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); - - vm.etch(address(0x0100), hex"60FF"); - - vm.startSnapshotGas("validation_r1_7212_replayable"); - MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); - uint256 gasUsed = vm.stopSnapshotGas(); - console2.log("test_r1_7212_replayable gas:", gasUsed); - } - - // R1 validation with FCL and cross-chain replayable nonce - function test_r1_FCL_replayable() public { - bytes[] memory owners = new bytes[](1); - owners[0] = passkeyOwner; - CoinbaseSmartWallet passkeyAccount = factory.createAccount(owners, 1); - - WebAuthn.WebAuthnAuth memory auth = createWebAuthnAuth(); - UserOperation memory op; - op.nonce = passkeyAccount.REPLAYABLE_NONCE_KEY() << 64; - op.callData = abi.encodeCall(CoinbaseSmartWallet.executeWithoutChainIdValidation, (new bytes[](1))); - op.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encode(auth))); - - vm.startSnapshotGas("validation_r1_FCL_replayable"); - MockEntryPoint(account.entryPoint()).validateUserOp(address(passkeyAccount), op, TEST_HASH, 0); - uint256 gasUsed = vm.stopSnapshotGas(); - console2.log("test_r1_FCL_replayable gas:", gasUsed); - } - - function createWebAuthnAuth() internal pure returns (WebAuthn.WebAuthnAuth memory) { - return WebAuthn.WebAuthnAuth({ - authenticatorData: hex"49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97630500000000", - clientDataJSON: '{"type":"webauthn.get","challenge":"', - challengeIndex: 23, - typeIndex: 1, - r: 0x7e7de1a8b53f9fea2e6b2b8f0e564c126a5b3a8f0e1234567890abcdef123456, - s: 0x3b2a5c8f5e4d6c7b8a9b0c1d2e3f4051627384950617283940516273849506ff - }); - } -} - -contract MockEntryPoint { - function validateUserOp(address account, UserOperation memory op, bytes32 hash, uint256 funds) - external returns (uint256) { - return CoinbaseSmartWallet(payable(account)).validateUserOp(op, hash, funds); - } -} \ No newline at end of file From b51effe48f9f2ccc147a72f3beae5005021cef33 Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Fri, 5 Sep 2025 18:43:35 -0700 Subject: [PATCH 5/8] done --- snapshots/EndToEndTest.json | 4 ++-- test/gas/EndToEnd.t.sol | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/snapshots/EndToEndTest.json b/snapshots/EndToEndTest.json index 3eed19d..28120cc 100644 --- a/snapshots/EndToEndTest.json +++ b/snapshots/EndToEndTest.json @@ -1,6 +1,6 @@ { - "e2e_transfer_erc20_4337": "159402", + "e2e_transfer_erc20_baseAccount": "159402", "e2e_transfer_erc20_eoa": "50910", - "e2e_transfer_native_4337": "159635", + "e2e_transfer_native_baseAccount": "134635", "e2e_transfer_native_eoa": "17652" } \ No newline at end of file diff --git a/test/gas/EndToEnd.t.sol b/test/gas/EndToEnd.t.sol index c5a6fc7..c9f5f1a 100644 --- a/test/gas/EndToEnd.t.sol +++ b/test/gas/EndToEnd.t.sol @@ -41,16 +41,20 @@ contract EndToEndTest is SmartWalletTestBase { // Native ETH transfer comparison between ERC-4337 and EOA function test_transfer_native() public { + // Dust recipient to control for gas increase for first non-zero balance + vm.deal(address(0x1234), 1 wei); + usdc.mint(eoaUser, 1 wei); + userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.execute, (address(0x1234), 1 ether, "")); UserOperation memory op = _getUserOpWithSignature(); bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); - console2.log("test_transfer_native ERC-4337 calldata size:", handleOpsCalldata.length); + console2.log("test_transfer_native Base Account calldata size:", handleOpsCalldata.length); - vm.startSnapshotGas("e2e_transfer_native_4337"); + vm.startSnapshotGas("e2e_transfer_native_baseAccount"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); - console2.log("test_transfer_native ERC-4337 gas:", gas4337); + console2.log("test_transfer_native Base Account gas:", gas4337); console2.log("test_transfer_native EOA calldata size:", uint256(0)); @@ -59,11 +63,15 @@ contract EndToEndTest is SmartWalletTestBase { payable(address(0x1234)).transfer(1 ether); uint256 gasEOA = vm.stopSnapshotGas(); console2.log("test_transfer_native EOA gas:", gasEOA); - console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); + console2.log("Gas overhead (4337 Base Account / EOA):", (gas4337 * 100) / gasEOA, "%"); } // ERC20 transfer comparison between ERC-4337 and EOA function test_transfer_erc20() public { + // Dust recipient to control for gas increase for first non-zero balance + vm.deal(address(0x5678), 1 wei); + usdc.mint(eoaUser, 1 wei); + userOpCalldata = abi.encodeCall( CoinbaseSmartWallet.execute, (address(usdc), 0, abi.encodeCall(usdc.transfer, (address(0x5678), 100e6))) @@ -71,12 +79,12 @@ contract EndToEndTest is SmartWalletTestBase { UserOperation memory op = _getUserOpWithSignature(); bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); - console2.log("test_transfer_erc20 ERC-4337 calldata size:", handleOpsCalldata.length); + console2.log("test_transfer_erc20 Base Account calldata size:", handleOpsCalldata.length); - vm.startSnapshotGas("e2e_transfer_erc20_4337"); + vm.startSnapshotGas("e2e_transfer_erc20_baseAccount"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); - console2.log("test_transfer_erc20 ERC-4337 gas:", gas4337); + console2.log("test_transfer_erc20 Base Account gas:", gas4337); bytes memory eoaCalldata = abi.encodeCall(usdc.transfer, (address(0x5678), 100e6)); console2.log("test_transfer_erc20 EOA calldata size:", eoaCalldata.length); @@ -86,7 +94,7 @@ contract EndToEndTest is SmartWalletTestBase { usdc.transfer(address(0x5678), 100e6); uint256 gasEOA = vm.stopSnapshotGas(); console2.log("test_transfer_erc20 EOA gas:", gasEOA); - console2.log("Gas overhead (4337/EOA):", (gas4337 * 100) / gasEOA, "%"); + console2.log("Gas overhead (4337 Base Account / EOA):", (gas4337 * 100) / gasEOA, "%"); } function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) { From 2e8c45bb96d52401acd0744d4f6bf4811a505a75 Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Mon, 8 Sep 2025 15:45:38 -0700 Subject: [PATCH 6/8] formatted --- test/gas/EndToEnd.t.sol | 52 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/gas/EndToEnd.t.sol b/test/gas/EndToEnd.t.sol index c9f5f1a..a143fb7 100644 --- a/test/gas/EndToEnd.t.sol +++ b/test/gas/EndToEnd.t.sol @@ -1,44 +1,45 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import {console2} from "forge-std/Test.sol"; -import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; +import {MockERC20} from "../../lib/solady/test/utils/mocks/MockERC20.sol"; import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol"; import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol"; +import {SmartWalletTestBase} from "../CoinbaseSmartWallet/SmartWalletTestBase.sol"; + +import {Static} from "../CoinbaseSmartWallet/Static.sol"; import {MockTarget} from "../mocks/MockTarget.sol"; import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol"; -import {MockERC20} from "../../lib/solady/test/utils/mocks/MockERC20.sol"; -import {Static} from "../CoinbaseSmartWallet/Static.sol"; +import {console2} from "forge-std/Test.sol"; /// forge-config: default.isolate = true contract EndToEndTest is SmartWalletTestBase { address eoaUser = address(0xe0a); - + MockERC20 usdc; MockTarget target; CoinbaseSmartWalletFactory factory; - + function setUp() public override { vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES); - + CoinbaseSmartWallet implementation = new CoinbaseSmartWallet(); factory = new CoinbaseSmartWalletFactory(address(implementation)); - + signerPrivateKey = 0xa11ce; signer = vm.addr(signerPrivateKey); owners.push(abi.encode(signer)); account = factory.createAccount(owners, 0); - + vm.deal(address(account), 100 ether); vm.deal(eoaUser, 100 ether); - + usdc = new MockERC20("USD Coin", "USDC", 6); usdc.mint(address(account), 10000e6); usdc.mint(eoaUser, 10000e6); - + target = new MockTarget(); } - + // Native ETH transfer comparison between ERC-4337 and EOA function test_transfer_native() public { // Dust recipient to control for gas increase for first non-zero balance @@ -47,17 +48,17 @@ contract EndToEndTest is SmartWalletTestBase { userOpCalldata = abi.encodeCall(CoinbaseSmartWallet.execute, (address(0x1234), 1 ether, "")); UserOperation memory op = _getUserOpWithSignature(); - + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); console2.log("test_transfer_native Base Account calldata size:", handleOpsCalldata.length); - + vm.startSnapshotGas("e2e_transfer_native_baseAccount"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); console2.log("test_transfer_native Base Account gas:", gas4337); - + console2.log("test_transfer_native EOA calldata size:", uint256(0)); - + vm.prank(eoaUser); vm.startSnapshotGas("e2e_transfer_native_eoa"); payable(address(0x1234)).transfer(1 ether); @@ -65,7 +66,7 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("test_transfer_native EOA gas:", gasEOA); console2.log("Gas overhead (4337 Base Account / EOA):", (gas4337 * 100) / gasEOA, "%"); } - + // ERC20 transfer comparison between ERC-4337 and EOA function test_transfer_erc20() public { // Dust recipient to control for gas increase for first non-zero balance @@ -73,22 +74,21 @@ contract EndToEndTest is SmartWalletTestBase { usdc.mint(eoaUser, 1 wei); userOpCalldata = abi.encodeCall( - CoinbaseSmartWallet.execute, - (address(usdc), 0, abi.encodeCall(usdc.transfer, (address(0x5678), 100e6))) + CoinbaseSmartWallet.execute, (address(usdc), 0, abi.encodeCall(usdc.transfer, (address(0x5678), 100e6))) ); UserOperation memory op = _getUserOpWithSignature(); - + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); console2.log("test_transfer_erc20 Base Account calldata size:", handleOpsCalldata.length); - + vm.startSnapshotGas("e2e_transfer_erc20_baseAccount"); _sendUserOperation(op); uint256 gas4337 = vm.stopSnapshotGas(); console2.log("test_transfer_erc20 Base Account gas:", gas4337); - + bytes memory eoaCalldata = abi.encodeCall(usdc.transfer, (address(0x5678), 100e6)); console2.log("test_transfer_erc20 EOA calldata size:", eoaCalldata.length); - + vm.prank(eoaUser); vm.startSnapshotGas("e2e_transfer_erc20_eoa"); usdc.transfer(address(0x5678), 100e6); @@ -96,16 +96,16 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("test_transfer_erc20 EOA gas:", gasEOA); console2.log("Gas overhead (4337 Base Account / EOA):", (gas4337 * 100) / gasEOA, "%"); } - + function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) { UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; return ops; } - + 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))); } -} \ No newline at end of file +} From 5577f7048a04d436261e449ab9aa3e8c728916c4 Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Mon, 8 Sep 2025 15:52:55 -0700 Subject: [PATCH 7/8] updated snapshot --- .gas-snapshot | 154 +++++++++++++++++++++++++------------------------- 1 file changed, 78 insertions(+), 76 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 782c3af..a8c3ace 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,76 +1,78 @@ -AddOwnerAddressTest:testEmitsAddOwner() (gas: 91954) -AddOwnerAddressTest:testIncreasesOwnerIndex() (gas: 90492) -AddOwnerAddressTest:testRevertsIfAlreadyOwner() (gas: 92327) -AddOwnerAddressTest:testRevertsIfCalledByNonOwner() (gas: 11831) -AddOwnerAddressTest:testSetsIsOwner() (gas: 90125) -AddOwnerAddressTest:testSetsOwnerAtIndex() (gas: 99961) -AddOwnerPublicKeyTest:testEmitsAddOwner() (gas: 115024) -AddOwnerPublicKeyTest:testFuzzIsOwnerPublicKey(bytes32,bytes32) (runs: 256, μ: 114454, ~: 114454) -AddOwnerPublicKeyTest:testRevertsIfAlreadyOwner() (gas: 115392) -AddOwnerPublicKeyTest:testRevertsIfCalledByNonOwner() (gas: 11895) -AddOwnerPublicKeyTest:testSetsIsOwner() (gas: 113193) -AddOwnerPublicKeyTest:testSetsOwnerAtIndex() (gas: 130925) -CoinbaseSmartWallet1271InputGeneratorTest:testGetReplaySafeHashForDeployedAccount() (gas: 311701) -CoinbaseSmartWallet1271InputGeneratorTest:testGetReplaySafeHashForUndeployedAccount() (gas: 293976) -CoinbaseSmartWalletFactoryTest:testDeployDeterministicPassValues() (gas: 270581) -CoinbaseSmartWalletFactoryTest:test_CreateAccount_ReturnsPredeterminedAddress_WhenAccountAlreadyExists() (gas: 289811) -CoinbaseSmartWalletFactoryTest:test_RevertsIfLength32ButLargerThanAddress() (gas: 303514) -CoinbaseSmartWalletFactoryTest:test_constructor_revertsIfImplementationIsNotDeployed(address) (runs: 256, μ: 39338, ~: 39348) -CoinbaseSmartWalletFactoryTest:test_constructor_setsImplementation(address) (runs: 256, μ: 455736, ~: 455736) -CoinbaseSmartWalletFactoryTest:test_createAccountDeploysToPredeterminedAddress() (gas: 271825) -CoinbaseSmartWalletFactoryTest:test_createAccountSetsOwnersCorrectly() (gas: 281476) -CoinbaseSmartWalletFactoryTest:test_createAccount_emitsAccountCreatedEvent(uint256) (runs: 256, μ: 273358, ~: 273358) -CoinbaseSmartWalletFactoryTest:test_exitIfAccountIsAlreadyInitialized() (gas: 271307) -CoinbaseSmartWalletFactoryTest:test_implementation_returnsExpectedAddress() (gas: 7698) -CoinbaseSmartWalletFactoryTest:test_initCodeHash() (gas: 7913) -CoinbaseSmartWalletFactoryTest:test_revertsIfNoOwners() (gas: 29256) -ERC1271Test:test_returnsExpectedDomainHashWhenProxy() (gas: 31630) -ERC1271Test:test_static() (gas: 4046108) -MultiOwnableInitializeTest:testRevertsIfLength32ButLargerThanAddress() (gas: 80861) -MultiOwnableInitializeTest:testRevertsIfLength32NotAddress() (gas: 81027) -MultiOwnableInitializeTest:testRevertsIfLengthNot32Or64() (gas: 103534) -RemoveLastOwnerTest:test_emitsRemoveOwner() (gas: 50105) -RemoveLastOwnerTest:test_removesOwner() (gas: 48993) -RemoveLastOwnerTest:test_removesOwnerAtIndex() (gas: 49127) -RemoveLastOwnerTest:test_revert_whenCalledByNonOwner(address) (runs: 256, μ: 19210, ~: 19210) -RemoveLastOwnerTest:test_revert_whenNoOwnerAtIndex() (gas: 48103) -RemoveLastOwnerTest:test_revert_whenWrongOwnerAtIndex() (gas: 34023) -RemoveLastOwnerTest:test_reverts_whenNotLastOwner() (gas: 123054) -RemoveOwnerAtIndexTest:test_emitsRemoveOwner() (gas: 55370) -RemoveOwnerAtIndexTest:test_removesOwner() (gas: 54324) -RemoveOwnerAtIndexTest:test_removesOwnerAtIndex() (gas: 54222) -RemoveOwnerAtIndexTest:test_revert_whenCalledByNonOwner(address) (runs: 256, μ: 19232, ~: 19232) -RemoveOwnerAtIndexTest:test_revert_whenNoOwnerAtIndex() (gas: 33098) -RemoveOwnerAtIndexTest:test_revert_whenWrongOwnerAtIndex() (gas: 36598) -RemoveOwnerAtIndexTest:test_reverts_ifIsLastOwner() (gas: 7632833) -TestCanSkipChainIdValidation:test_approvedSelectorsReturnTrue() (gas: 17685) -TestCanSkipChainIdValidation:test_otherSelectorsReturnFalse() (gas: 12561) -TestExecuteWithoutChainIdValidation:testExecute() (gas: 485146) -TestExecuteWithoutChainIdValidation:testExecuteBatch() (gas: 889868) -TestExecuteWithoutChainIdValidation:testExecuteBatch(uint256) (runs: 256, μ: 4544535, ~: 4457018) -TestExecuteWithoutChainIdValidation:test__codesize() (gas: 61710) -TestExecuteWithoutChainIdValidation:test_revertsWithReservedNonce() (gas: 81700) -TestExecuteWithoutChainIdValidation:test_reverts_whenCallerNotEntryPoint() (gas: 11148) -TestExecuteWithoutChainIdValidation:test_reverts_whenOneCallReverts() (gas: 467006) -TestExecuteWithoutChainIdValidation:test_reverts_whenOneSelectorNotApproved() (gas: 179933) -TestExecuteWithoutChainIdValidation:test_reverts_whenSelectorNotApproved() (gas: 106565) -TestExecuteWithoutChainIdValidation:test_succeeds_whenSelectorAllowed() (gas: 424623) -TestImplementation:testImplementation() (gas: 12677) -TestInitialize:testInitialize() (gas: 21146) -TestInitialize:test_cannotInitImplementation() (gas: 3676943) -TestIsValidSignature:testReturnsInvalidIfPasskeySigButWrongOwnerLength() (gas: 40556) -TestIsValidSignature:testRevertsIfEthereumSignatureButWrongOwnerLength() (gas: 24272) -TestIsValidSignature:testRevertsIfOwnerIsInvalidEthereumAddress() (gas: 22001) -TestIsValidSignature:testSmartWalletSigner() (gas: 3967929) -TestIsValidSignature:testValidateSignatureWithEOASigner() (gas: 25053) -TestIsValidSignature:testValidateSignatureWithEOASignerFailsWithWrongSigner() (gas: 23780) -TestIsValidSignature:testValidateSignatureWithPasskeySigner() (gas: 354806) -TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsBadOwnerIndex() (gas: 35960) -TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsWithWrongBadSignature() (gas: 345393) -TestUpgradeToAndCall:testUpgradeToAndCall() (gas: 25322) -TestValidateUserOp:test_reverts_whenReplayableNonceKeyInvalidForSelector() (gas: 14609) -TestValidateUserOp:test_reverts_whenSelectorInvalidForReplayableNonceKey() (gas: 14469) -TestValidateUserOp:test_reverts_whenUpgradeToImplementationWithNoCode(address) (runs: 256, μ: 23208, ~: 23228) -TestValidateUserOp:test_succeedsWithEOASigner() (gas: 456302) -TestValidateUserOp:test_succeedsWithPasskeySigner() (gas: 711409) -TestValidateUserOp:test_succeeds_whenUpgradeToImplementationWithCode() (gas: 3714211) \ No newline at end of file +AddOwnerAddressTest:testEmitsAddOwner() (gas: 95829) +AddOwnerAddressTest:testIncreasesOwnerIndex() (gas: 93811) +AddOwnerAddressTest:testRevertsIfAlreadyOwner() (gas: 97900) +AddOwnerAddressTest:testRevertsIfCalledByNonOwner() (gas: 13315) +AddOwnerAddressTest:testSetsIsOwner() (gas: 94340) +AddOwnerAddressTest:testSetsOwnerAtIndex() (gas: 97345) +AddOwnerPublicKeyTest:testEmitsAddOwner() (gas: 119224) +AddOwnerPublicKeyTest:testFuzzIsOwnerPublicKey(bytes32,bytes32) (runs: 260, μ: 120409, ~: 120409) +AddOwnerPublicKeyTest:testRevertsIfAlreadyOwner() (gas: 121567) +AddOwnerPublicKeyTest:testRevertsIfCalledByNonOwner() (gas: 13468) +AddOwnerPublicKeyTest:testSetsIsOwner() (gas: 117612) +AddOwnerPublicKeyTest:testSetsOwnerAtIndex() (gas: 120856) +CoinbaseSmartWallet1271InputGeneratorTest:testGetReplaySafeHashForDeployedAccount() (gas: 321780) +CoinbaseSmartWallet1271InputGeneratorTest:testGetReplaySafeHashForUndeployedAccount() (gas: 306564) +CoinbaseSmartWalletFactoryTest:testDeployDeterministicPassValues() (gas: 278647) +CoinbaseSmartWalletFactoryTest:test_CreateAccount_ReturnsPredeterminedAddress_WhenAccountAlreadyExists() (gas: 302620) +CoinbaseSmartWalletFactoryTest:test_RevertsIfLength32ButLargerThanAddress() (gas: 312891) +CoinbaseSmartWalletFactoryTest:test_constructor_revertsIfImplementationIsNotDeployed(address) (runs: 260, μ: 40036, ~: 40046) +CoinbaseSmartWalletFactoryTest:test_constructor_setsImplementation(address) (runs: 260, μ: 548238, ~: 548238) +CoinbaseSmartWalletFactoryTest:test_createAccountDeploysToPredeterminedAddress() (gas: 281841) +CoinbaseSmartWalletFactoryTest:test_createAccountSetsOwnersCorrectly() (gas: 293368) +CoinbaseSmartWalletFactoryTest:test_createAccount_emitsAccountCreatedEvent(uint256) (runs: 260, μ: 284216, ~: 284216) +CoinbaseSmartWalletFactoryTest:test_exitIfAccountIsAlreadyInitialized() (gas: 282240) +CoinbaseSmartWalletFactoryTest:test_implementation_returnsExpectedAddress() (gas: 7992) +CoinbaseSmartWalletFactoryTest:test_initCodeHash() (gas: 8307) +CoinbaseSmartWalletFactoryTest:test_revertsIfNoOwners() (gas: 30019) +ERC1271Test:test_returnsExpectedDomainHashWhenProxy() (gas: 23618) +ERC1271Test:test_static() (gas: 5038825) +EndToEndTest:test_transfer_erc20() (gas: 462176) +EndToEndTest:test_transfer_native() (gas: 355253) +MultiOwnableInitializeTest:testRevertsIfLength32ButLargerThanAddress() (gas: 83336) +MultiOwnableInitializeTest:testRevertsIfLength32NotAddress() (gas: 83255) +MultiOwnableInitializeTest:testRevertsIfLengthNot32Or64() (gas: 105695) +RemoveLastOwnerTest:test_emitsRemoveOwner() (gas: 52621) +RemoveLastOwnerTest:test_removesOwner() (gas: 52383) +RemoveLastOwnerTest:test_removesOwnerAtIndex() (gas: 53221) +RemoveLastOwnerTest:test_revert_whenCalledByNonOwner(address) (runs: 260, μ: 20652, ~: 20652) +RemoveLastOwnerTest:test_revert_whenNoOwnerAtIndex() (gas: 50014) +RemoveLastOwnerTest:test_revert_whenWrongOwnerAtIndex() (gas: 37450) +RemoveLastOwnerTest:test_reverts_whenNotLastOwner() (gas: 127056) +RemoveOwnerAtIndexTest:test_emitsRemoveOwner() (gas: 57775) +RemoveOwnerAtIndexTest:test_removesOwner() (gas: 57349) +RemoveOwnerAtIndexTest:test_removesOwnerAtIndex() (gas: 57914) +RemoveOwnerAtIndexTest:test_revert_whenCalledByNonOwner(address) (runs: 260, μ: 20674, ~: 20674) +RemoveOwnerAtIndexTest:test_revert_whenNoOwnerAtIndex() (gas: 35018) +RemoveOwnerAtIndexTest:test_revert_whenWrongOwnerAtIndex() (gas: 40657) +RemoveOwnerAtIndexTest:test_reverts_ifIsLastOwner() (gas: 8529792) +TestCanSkipChainIdValidation:test_approvedSelectorsReturnTrue() (gas: 20637) +TestCanSkipChainIdValidation:test_otherSelectorsReturnFalse() (gas: 13631) +TestExecuteWithoutChainIdValidation:testExecute() (gas: 587208) +TestExecuteWithoutChainIdValidation:testExecuteBatch() (gas: 1087434) +TestExecuteWithoutChainIdValidation:testExecuteBatch(uint256) (runs: 260, μ: 5622658, ~: 5415134) +TestExecuteWithoutChainIdValidation:test__codesize() (gas: 65702) +TestExecuteWithoutChainIdValidation:test_revertsWithReservedNonce() (gas: 87228) +TestExecuteWithoutChainIdValidation:test_reverts_whenCallerNotEntryPoint() (gas: 11407) +TestExecuteWithoutChainIdValidation:test_reverts_whenOneCallReverts() (gas: 475054) +TestExecuteWithoutChainIdValidation:test_reverts_whenOneSelectorNotApproved() (gas: 183931) +TestExecuteWithoutChainIdValidation:test_reverts_whenSelectorNotApproved() (gas: 108819) +TestExecuteWithoutChainIdValidation:test_succeeds_whenSelectorAllowed() (gas: 440590) +TestImplementation:testImplementation() (gas: 12896) +TestInitialize:testInitialize() (gas: 22939) +TestInitialize:test_cannotInitImplementation() (gas: 4597308) +TestIsValidSignature:testReturnsInvalidIfPasskeySigButWrongOwnerLength() (gas: 50912) +TestIsValidSignature:testRevertsIfEthereumSignatureButWrongOwnerLength() (gas: 29788) +TestIsValidSignature:testRevertsIfOwnerIsInvalidEthereumAddress() (gas: 29384) +TestIsValidSignature:testSmartWalletSigner() (gas: 4950306) +TestIsValidSignature:testValidateSignatureWithEOASigner() (gas: 30653) +TestIsValidSignature:testValidateSignatureWithEOASignerFailsWithWrongSigner() (gas: 27974) +TestIsValidSignature:testValidateSignatureWithPasskeySigner() (gas: 275434) +TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsBadOwnerIndex() (gas: 46521) +TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsWithWrongBadSignature() (gas: 270588) +TestUpgradeToAndCall:testUpgradeToAndCall() (gas: 26537) +TestValidateUserOp:test_reverts_whenReplayableNonceKeyInvalidForSelector() (gas: 17983) +TestValidateUserOp:test_reverts_whenSelectorInvalidForReplayableNonceKey() (gas: 19057) +TestValidateUserOp:test_reverts_whenUpgradeToImplementationWithNoCode(address) (runs: 260, μ: 30315, ~: 30315) +TestValidateUserOp:test_succeedsWithEOASigner() (gas: 687856) +TestValidateUserOp:test_succeedsWithPasskeySigner() (gas: 854583) +TestValidateUserOp:test_succeeds_whenUpgradeToImplementationWithCode() (gas: 4688457) \ No newline at end of file From 87fa54143c33e238e26cad256cb0fd64b4fdcfee Mon Sep 17 00:00:00 2001 From: Shamit Surana Date: Tue, 9 Sep 2025 16:44:56 -0700 Subject: [PATCH 8/8] Added swapping --- .gitmodules | 3 + foundry.lock | 6 ++ foundry.toml | 2 +- lib/v4-core | 1 + remappings.txt | 15 +++++ snapshots/EndToEndTest.json | 10 +-- src/CoinbaseSmartWallet.sol | 2 +- test/gas/EndToEnd.t.sol | 129 ++++++++++++++++++++++++++++++++++-- 8 files changed, 158 insertions(+), 10 deletions(-) create mode 160000 lib/v4-core create mode 100644 remappings.txt diff --git a/.gitmodules b/.gitmodules index f6e778a..afc65bd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "lib/safe-singleton-deployer-sol"] path = lib/safe-singleton-deployer-sol url = https://github.com/wilsoncusack/safe-singleton-deployer-sol +[submodule "lib/v4-core"] + path = lib/v4-core + url = https://github.com/Uniswap/v4-core diff --git a/foundry.lock b/foundry.lock index 4be77fe..0c69790 100644 --- a/foundry.lock +++ b/foundry.lock @@ -20,6 +20,12 @@ "lib/solady": { "rev": "c4c96607cb3aa3807b14c81ae2015bcba061f8fc" }, + "lib/v4-core": { + "tag": { + "name": "v4.0.0", + "rev": "e50237c43811bd9b526eff40f26772152a42daba" + } + }, "lib/webauthn-sol": { "rev": "619f20ab0f074fef41066ee4ab24849a913263b2" } diff --git a/foundry.toml b/foundry.toml index 0846d5e..f3a6113 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,7 +8,7 @@ optimizer = true optimizer_runs = 999999 via_ir = true evm_version = "prague" -solc_version = "0.8.23" +solc_version = "0.8.26" [fmt] sort_imports = true diff --git a/lib/v4-core b/lib/v4-core new file mode 160000 index 0000000..e50237c --- /dev/null +++ b/lib/v4-core @@ -0,0 +1 @@ +Subproject commit e50237c43811bd9b526eff40f26772152a42daba diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 0000000..c60d2e3 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,15 @@ +@ensdomains/=lib/v4-core/node_modules/@ensdomains/ +@openzeppelin/=lib/v4-core/lib/openzeppelin-contracts/ +FreshCryptoLib/=lib/webauthn-sol/lib/FreshCryptoLib/solidity/src/ +account-abstraction/=lib/account-abstraction/contracts/ +ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +hardhat/=lib/v4-core/node_modules/hardhat/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +p256-verifier/=lib/p256-verifier/ +safe-singleton-deployer-sol/=lib/safe-singleton-deployer-sol/ +solady/=lib/solady/src/ +solmate/=lib/v4-core/lib/solmate/ +v4-core/=lib/v4-core/src/ +webauthn-sol/=lib/webauthn-sol/src/ diff --git a/snapshots/EndToEndTest.json b/snapshots/EndToEndTest.json index 28120cc..28edf88 100644 --- a/snapshots/EndToEndTest.json +++ b/snapshots/EndToEndTest.json @@ -1,6 +1,8 @@ { - "e2e_transfer_erc20_baseAccount": "159402", - "e2e_transfer_erc20_eoa": "50910", - "e2e_transfer_native_baseAccount": "134635", - "e2e_transfer_native_eoa": "17652" + "e2e_swap_baseAccount": "388633", + "e2e_swap_eoa": "176712", + "e2e_transfer_erc20_baseAccount": "158011", + "e2e_transfer_erc20_eoa": "50584", + "e2e_transfer_native_baseAccount": "133676", + "e2e_transfer_native_eoa": "17326" } \ No newline at end of file diff --git a/src/CoinbaseSmartWallet.sol b/src/CoinbaseSmartWallet.sol index a3b02fd..a11eb25 100644 --- a/src/CoinbaseSmartWallet.sol +++ b/src/CoinbaseSmartWallet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.23; +pragma solidity 0.8.26; import {IAccount} from "account-abstraction/interfaces/IAccount.sol"; diff --git a/test/gas/EndToEnd.t.sol b/test/gas/EndToEnd.t.sol index a143fb7..a3497d2 100644 --- a/test/gas/EndToEnd.t.sol +++ b/test/gas/EndToEnd.t.sol @@ -11,13 +11,38 @@ import {MockTarget} from "../mocks/MockTarget.sol"; import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol"; import {console2} from "forge-std/Test.sol"; +// Uniswap v4 imports +import {PoolManager} from "v4-core/PoolManager.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol"; +import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {TickMath} from "v4-core/libraries/TickMath.sol"; +import {PoolModifyLiquidityTest} from "v4-core/test/PoolModifyLiquidityTest.sol"; +import {LiquidityAmounts} from "../../lib/v4-core/test/utils/LiquidityAmounts.sol"; + /// forge-config: default.isolate = true contract EndToEndTest is SmartWalletTestBase { + using CurrencyLibrary for Currency; + address eoaUser = address(0xe0a); MockERC20 usdc; + MockERC20 weth; MockTarget target; CoinbaseSmartWalletFactory factory; + + // Uniswap v4 contracts + IPoolManager poolManager; + PoolSwapTest swapRouter; + PoolModifyLiquidityTest modifyLiquidityRouter; + PoolKey poolKey; + + uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336; // sqrt(1) * 2^96 + uint160 constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1; + uint160 constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1; function setUp() public override { vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES); @@ -34,15 +59,28 @@ contract EndToEndTest is SmartWalletTestBase { vm.deal(eoaUser, 100 ether); usdc = new MockERC20("USD Coin", "USDC", 6); + weth = new MockERC20("Wrapped Ether", "WETH", 18); + usdc.mint(address(account), 10000e6); usdc.mint(eoaUser, 10000e6); + weth.mint(address(account), 100e18); + weth.mint(eoaUser, 100e18); + + // For liquidity provision + usdc.mint(address(this), 1000000e6); + weth.mint(address(this), 1000e18); target = new MockTarget(); + + poolManager = new PoolManager(address(this)); + swapRouter = new PoolSwapTest(poolManager); + modifyLiquidityRouter = new PoolModifyLiquidityTest(poolManager); + + setupUniswapV4Pool(); } - // Native ETH transfer comparison between ERC-4337 and EOA function test_transfer_native() public { - // Dust recipient to control for gas increase for first non-zero balance + // Dust to avoid first-time transfer gas cost vm.deal(address(0x1234), 1 wei); usdc.mint(eoaUser, 1 wei); @@ -67,9 +105,7 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337 Base Account / EOA):", (gas4337 * 100) / gasEOA, "%"); } - // ERC20 transfer comparison between ERC-4337 and EOA function test_transfer_erc20() public { - // Dust recipient to control for gas increase for first non-zero balance vm.deal(address(0x5678), 1 wei); usdc.mint(eoaUser, 1 wei); @@ -97,6 +133,51 @@ contract EndToEndTest is SmartWalletTestBase { console2.log("Gas overhead (4337 Base Account / EOA):", (gas4337 * 100) / gasEOA, "%"); } + function test_swap() public { + vm.prank(address(account)); + usdc.approve(address(swapRouter), type(uint256).max); + vm.prank(address(account)); + weth.approve(address(swapRouter), type(uint256).max); + + vm.prank(eoaUser); + usdc.approve(address(swapRouter), type(uint256).max); + vm.prank(eoaUser); + weth.approve(address(swapRouter), type(uint256).max); + + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: true, + amountSpecified: -1000e6, // Exact input: 1000 USDC + sqrtPriceLimitX96: MIN_PRICE_LIMIT + }); + bytes memory swapCalldata = abi.encodeCall( + PoolSwapTest.swap, + (poolKey, params, PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), "") + ); + + userOpCalldata = abi.encodeCall( + CoinbaseSmartWallet.execute, + (address(swapRouter), 0, swapCalldata) + ); + UserOperation memory op = _getUserOpWithSignature(); + + bytes memory handleOpsCalldata = abi.encodeCall(entryPoint.handleOps, (_makeOpsArray(op), payable(bundler))); + console2.log("test_swap Base Account calldata size:", handleOpsCalldata.length); + + vm.startSnapshotGas("e2e_swap_baseAccount"); + _sendUserOperation(op); + uint256 gas4337 = vm.stopSnapshotGas(); + console2.log("test_swap Base Account gas:", gas4337); + + console2.log("test_swap EOA calldata size:", swapCalldata.length); + + vm.prank(eoaUser); + vm.startSnapshotGas("e2e_swap_eoa"); + swapRouter.swap(poolKey, params, PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), ""); + uint256 gasEOA = vm.stopSnapshotGas(); + console2.log("test_swap EOA gas:", gasEOA); + console2.log("Gas overhead (4337 Base Account / EOA):", (gas4337 * 100) / gasEOA, "%"); + } + function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) { UserOperation[] memory ops = new UserOperation[](1); ops[0] = op; @@ -108,4 +189,44 @@ contract EndToEndTest is SmartWalletTestBase { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, toSign); signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v))); } + + function setupUniswapV4Pool() internal { + Currency currency0; + Currency currency1; + + if (address(usdc) < address(weth)) { + currency0 = Currency.wrap(address(usdc)); + currency1 = Currency.wrap(address(weth)); + } else { + currency0 = Currency.wrap(address(weth)); + currency1 = Currency.wrap(address(usdc)); + } + + poolKey = PoolKey({ + currency0: currency0, + currency1: currency1, + fee: 3000, + tickSpacing: 60, + hooks: IHooks(address(0)) + }); + + // sqrtPriceX96 = sqrt(10^12) * 2^96 for USDC/WETH decimal adjustment + uint160 sqrtPriceX96 = 79228162514264337593543950336 * 1e6; + poolManager.initialize(poolKey, sqrtPriceX96); + + usdc.approve(address(modifyLiquidityRouter), type(uint256).max); + weth.approve(address(modifyLiquidityRouter), type(uint256).max); + + IPoolManager.ModifyLiquidityParams memory params = IPoolManager.ModifyLiquidityParams({ + tickLower: -887220, + tickUpper: 887220, + liquidityDelta: 1000e6, + salt: 0 + }); + + modifyLiquidityRouter.modifyLiquidity(poolKey, params, ""); + + usdc.approve(address(poolManager), type(uint256).max); + weth.approve(address(poolManager), type(uint256).max); + } }