Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
"lib/solady": {
"rev": "5ea5d9f57ed6d24a27d00934f4a3448def931415"
},
"lib/v4-core": {
"tag": {
"name": "v4.0.0",
"rev": "e50237c43811bd9b526eff40f26772152a42daba"
}
},
"lib/webauthn-sol": {
"rev": "619f20ab0f074fef41066ee4ab24849a913263b2"
}
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/v4-core
Submodule v4-core added at e50237
8 changes: 6 additions & 2 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
@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/p256-verifier/lib/forge-std/lib/ds-test/src/
ds-test/=lib/v4-core/lib/forge-std/lib/ds-test/src/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these ds-test thrashes are weird but i'll trust what's here if it builds

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/
10 changes: 6 additions & 4 deletions snapshots/EndToEndTest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"e2e_transfer_erc20_baseAccount": "142302",
"e2e_transfer_erc20_eoa": "39910",
"e2e_transfer_native_baseAccount": "134635",
"e2e_transfer_native_eoa": "9338"
"e2e_swap_baseAccount": "329071",
"e2e_swap_eoa": "140219",
"e2e_transfer_erc20_baseAccount": "136663",
"e2e_transfer_erc20_eoa": "38849",
"e2e_transfer_native_baseAccount": "129947",
"e2e_transfer_native_eoa": "9357"
}
2 changes: 1 addition & 1 deletion src/CoinbaseSmartWallet.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
pragma solidity ^0.8.0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't change any CoinbaseSmartWallet source code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uniswap v4 requires solidity > v0.8.26


import {IAccount} from "account-abstraction/interfaces/IAccount.sol";

Expand Down
163 changes: 155 additions & 8 deletions test/gas/EndToEnd.t.sol
Original file line number Diff line number Diff line change
@@ -1,28 +1,49 @@
// 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";
import {MockTarget} from "../mocks/MockTarget.sol";
import {MockERC20} from "../../lib/solady/test/utils/mocks/MockERC20.sol";
import {CoinbaseSmartWallet} from "../../src/CoinbaseSmartWallet.sol";
import {CoinbaseSmartWalletFactory} from "../../src/CoinbaseSmartWalletFactory.sol";
import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";
import {console2} from "forge-std/Test.sol";
import {PoolManager} from "v4-core/PoolManager.sol";
import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {PoolModifyLiquidityTest} from "v4-core/test/PoolModifyLiquidityTest.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {PoolKey} from "v4-core/types/PoolKey.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 {
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is actually used anywhere

uint160 constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1;
uint160 constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1;

function setUp() public override {
// Deploy EntryPoint at canonical address
vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES);
Expand All @@ -43,10 +64,24 @@ contract EndToEndTest is SmartWalletTestBase {

// Deploy and mint USDC tokens
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 - Base Account
Expand Down Expand Up @@ -125,6 +160,71 @@ contract EndToEndTest is SmartWalletTestBase {
console2.log("test_transfer_erc20 EOA gas:", gasUsed);
}

// Uniswap v4 Swap - Base Account
function test_swap_baseAccount() public {
// Approve swap router to spend tokens
vm.prank(address(account));
usdc.approve(address(swapRouter), type(uint256).max);

// Configure swap parameters: swap 1000 USDC for WETH
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: true,
amountSpecified: -1000e6, // Exact input: 1000 USDC
sqrtPriceLimitX96: MIN_PRICE_LIMIT
});

// Prepare swap calldata
bytes memory swapCalldata = abi.encodeCall(
PoolSwapTest.swap,
(poolKey, params, PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), "")
);

// Wrap swap call in UserOperation
userOpCalldata = abi.encodeCall(
CoinbaseSmartWallet.execute,
(address(swapRouter), 0, swapCalldata)
);
UserOperation memory op = _getUserOpWithSignature();

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

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

// Uniswap v4 Swap - EOA
function test_swap_eoa() public {
// Approve swap router to spend tokens
vm.prank(eoaUser);
usdc.approve(address(swapRouter), type(uint256).max);

// Configure swap parameters: swap 1000 USDC for WETH
IPoolManager.SwapParams memory params = IPoolManager.SwapParams({
zeroForOne: true,
amountSpecified: -1000e6, // Exact input: 1000 USDC
sqrtPriceLimitX96: MIN_PRICE_LIMIT
});

// Measure calldata size
bytes memory swapCalldata = abi.encodeCall(
PoolSwapTest.swap,
(poolKey, params, PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), "")
);
console2.log("test_swap EOA calldata size:", swapCalldata.length);

// Execute and measure gas
vm.prank(eoaUser);
vm.startSnapshotGas("e2e_swap_eoa");
swapRouter.swap(poolKey, params, PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}), "");
uint256 gasUsed = vm.stopSnapshotGas();
console2.log("test_swap EOA gas:", gasUsed);
}

// Helper Functions
// Creates an array containing a single UserOperation
function _makeOpsArray(UserOperation memory op) internal pure returns (UserOperation[] memory) {
Expand All @@ -140,4 +240,51 @@ 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)));
}

// Sets up a Uniswap v4 USDC/WETH pool for swap testing
function setupUniswapV4Pool() internal {
Currency currency0;
Currency currency1;

// Ensure consistent token ordering (lower address first)
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));
}

// Configure pool parameters
poolKey = PoolKey({
currency0: currency0,
currency1: currency1,
fee: 3000, // 0.3% fee tier
tickSpacing: 60, // Standard spacing for 0.3% pools
hooks: IHooks(address(0)) // No hooks
});

// Initialize pool with 1:1000000 price ratio (accounting for USDC 6 decimals vs WETH 18 decimals)
// sqrtPriceX96 = sqrt(10^12) * 2^96 for USDC/WETH decimal adjustment
uint160 sqrtPriceX96 = SQRT_PRICE_1_1 * 1e6;
poolManager.initialize(poolKey, sqrtPriceX96);

// Approve liquidity router to add initial liquidity
usdc.approve(address(modifyLiquidityRouter), type(uint256).max);
weth.approve(address(modifyLiquidityRouter), type(uint256).max);

// Add liquidity to the pool across a wide price range
IPoolManager.ModifyLiquidityParams memory params = IPoolManager.ModifyLiquidityParams({
tickLower: -887220, // Min tick for full range
tickUpper: 887220, // Max tick for full range
liquidityDelta: 1000e6, // Amount of liquidity to add
salt: 0
});

modifyLiquidityRouter.modifyLiquidity(poolKey, params, "");

// Additional approvals for pool manager (may be needed for certain operations)
usdc.approve(address(poolManager), type(uint256).max);
weth.approve(address(poolManager), type(uint256).max);
}
}
Loading