Skip to content

Commit be89153

Browse files
authored
Merge pull request #13 from base-org/amie/refactor-tests
Test refactors
2 parents 14f4c8c + 2d4155d commit be89153

File tree

10 files changed

+735
-242
lines changed

10 files changed

+735
-242
lines changed

script/Initialize.s.sol

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {console} from "forge-std/console.sol";
66
import {CoinbaseSmartWallet} from "../lib/smart-wallet/src/CoinbaseSmartWallet.sol";
77
import {EIP7702Proxy} from "../src/EIP7702Proxy.sol";
88
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
9+
910
/**
1011
* This script tests an upgraded EOA by verifying ownership and executing an ETH transfer
1112
*
@@ -19,7 +20,7 @@ import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.s
1920
* - PROXY_TEMPLATE_ADDRESS_ODYSSEY: Address of the deployed proxy template on Odyssey
2021
*
2122
* Running instructions:
22-
*
23+
*
2324
* Local testing:
2425
* ```bash
2526
* forge script script/Initialize.s.sol --rpc-url http://localhost:8545 --broadcast --ffi
@@ -33,20 +34,26 @@ import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.s
3334
contract Initialize is Script {
3435
// Anvil's default funded accounts (for local testing)
3536
address constant _ANVIL_EOA = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8;
36-
uint256 constant _ANVIL_EOA_PK = 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;
37+
uint256 constant _ANVIL_EOA_PK =
38+
0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d;
3739
// Using another Anvil account as the new owner
38-
address constant _ANVIL_NEW_OWNER = 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
39-
uint256 constant _ANVIL_NEW_OWNER_PK = 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a;
40+
address constant _ANVIL_NEW_OWNER =
41+
0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC;
42+
uint256 constant _ANVIL_NEW_OWNER_PK =
43+
0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a;
4044
// Using the deployer account as recipient for test transactions
41-
address constant _ANVIL_DEPLOYER = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
42-
uint256 constant _ANVIL_DEPLOYER_PK = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
45+
address constant _ANVIL_DEPLOYER =
46+
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266;
47+
uint256 constant _ANVIL_DEPLOYER_PK =
48+
0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
4349

4450
// Chain IDs
4551
uint256 constant _ANVIL_CHAIN_ID = 31337;
4652
uint256 constant _ODYSSEY_CHAIN_ID = 911867;
4753

4854
// Deterministic proxy address for Anvil environment
49-
address constant _PROXY_ADDRESS_ANVIL = 0x2d95f129bCEbD5cF7f395c7B34106ac1DCfb0CA9;
55+
address constant _PROXY_ADDRESS_ANVIL =
56+
0x2d95f129bCEbD5cF7f395c7B34106ac1DCfb0CA9;
5057

5158
function run() external {
5259
// Determine which environment we're in
@@ -84,7 +91,10 @@ contract Initialize is Script {
8491
console.log("Using proxy template at:", proxyAddr);
8592

8693
// First verify the EOA has code
87-
require(address(eoa).code.length > 0, "EOA not upgraded yet! Run UpgradeEOA.s.sol first");
94+
require(
95+
address(eoa).code.length > 0,
96+
"EOA not upgraded yet! Run UpgradeEOA.s.sol first"
97+
);
8898
console.log("[OK] Verified EOA has been upgraded");
8999

90100
// Create and sign the initialize data with just the new owner
@@ -93,28 +103,30 @@ contract Initialize is Script {
93103
bytes memory initArgs = abi.encode(owners);
94104
bytes32 initHash = keccak256(abi.encode(proxyAddr, initArgs));
95105
(uint8 v, bytes32 r, bytes32 s) = vm.sign(eoaPk, initHash);
96-
106+
97107
bytes memory initSignature = abi.encodePacked(r, s, v);
98-
108+
99109
// Try to recover ourselves before sending
100110
address recovered = ECDSA.recover(initHash, initSignature);
101111
console.log("Recovered:", recovered);
102112
require(recovered == eoa, "Signature recovery failed - wrong signer");
103113

104114
// Start broadcast with EOA's private key to call initialize
105115
vm.startBroadcast(eoaPk);
106-
116+
107117
// Try to initialize, but handle the case where it's already initialized
108118
try EIP7702Proxy(payable(eoa)).initialize(initArgs, initSignature) {
109119
console.log("[OK] Successfully initialized the smart wallet");
110120
} catch Error(string memory reason) {
111121
console.log("[INFO] Initialize call reverted with reason:", reason);
112122
} catch (bytes memory) {
113-
console.log("[INFO] Initialization failed: EOA may already have been initialized");
123+
console.log(
124+
"[INFO] Initialization failed: EOA may already have been initialized"
125+
);
114126
}
115127

116128
vm.stopBroadcast();
117-
129+
118130
// Verify ownership for the new owner
119131
CoinbaseSmartWallet smartWallet = CoinbaseSmartWallet(payable(eoa));
120132
bool isNewOwner = smartWallet.isOwnerAddress(newOwner);
@@ -128,13 +140,13 @@ contract Initialize is Script {
128140
console.log("Deployer balance before:", deployerBalanceBefore);
129141

130142
vm.startBroadcast(newOwnerPk);
131-
143+
132144
// Empty calldata for a simple ETH transfer
133145
bytes memory callData = "";
134146
smartWallet.execute(
135-
payable(deployer), // target: sending to the deployer
136-
amount, // value: amount of ETH to send
137-
callData // data: empty for simple ETH transfer
147+
payable(deployer), // target: sending to the deployer
148+
amount, // value: amount of ETH to send
149+
callData // data: empty for simple ETH transfer
138150
);
139151

140152
uint256 deployerBalanceAfter = deployer.balance;
@@ -147,4 +159,4 @@ contract Initialize is Script {
147159

148160
vm.stopBroadcast();
149161
}
150-
}
162+
}

script/UpgradeEOA.s.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: UNLICENSED
2-
pragma solidity ^0.8.0;
2+
pragma solidity ^0.8.23;
33

44
import {Script} from "forge-std/Script.sol";
55
import {console} from "forge-std/console.sol";

src/EIP7702Proxy.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: UNLICENSED
2-
pragma solidity ^0.8.0;
2+
pragma solidity ^0.8.23;
33

44
import {Proxy} from "openzeppelin-contracts/contracts/proxy/Proxy.sol";
55
import {ERC1967Utils} from "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol";
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.23;
3+
4+
import {Test} from "forge-std/Test.sol";
5+
import {EIP7702Proxy} from "../../src/EIP7702Proxy.sol";
6+
import {CoinbaseSmartWallet} from "../../lib/smart-wallet/src/CoinbaseSmartWallet.sol";
7+
import {ECDSA} from "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";
8+
9+
/**
10+
* @title CoinbaseImplementationTest
11+
* @dev Tests specific to the CoinbaseSmartWallet implementation
12+
*/
13+
contract CoinbaseImplementationTest is Test {
14+
uint256 constant _EOA_PRIVATE_KEY = 0xA11CE;
15+
address payable _eoa;
16+
17+
uint256 constant _NEW_OWNER_PRIVATE_KEY = 0xB0B;
18+
address payable _newOwner;
19+
20+
CoinbaseSmartWallet wallet;
21+
CoinbaseSmartWallet implementation;
22+
EIP7702Proxy proxy;
23+
bytes4 initSelector;
24+
25+
bytes4 constant ERC1271_MAGIC_VALUE = 0x1626ba7e;
26+
bytes4 constant ERC1271_FAIL_VALUE = 0xffffffff;
27+
28+
function setUp() public {
29+
// Set up test accounts
30+
_eoa = payable(vm.addr(_EOA_PRIVATE_KEY));
31+
_newOwner = payable(vm.addr(_NEW_OWNER_PRIVATE_KEY));
32+
33+
// Deploy Coinbase implementation
34+
implementation = new CoinbaseSmartWallet();
35+
initSelector = CoinbaseSmartWallet.initialize.selector;
36+
37+
// Deploy and setup proxy
38+
proxy = new EIP7702Proxy(address(implementation), initSelector);
39+
bytes memory proxyCode = address(proxy).code;
40+
vm.etch(_eoa, proxyCode);
41+
42+
// Initialize with Coinbase implementation
43+
bytes memory initArgs = _createInitArgs(_newOwner);
44+
bytes memory signature = _signInitData(_EOA_PRIVATE_KEY, initArgs);
45+
EIP7702Proxy(_eoa).initialize(initArgs, signature);
46+
47+
wallet = CoinbaseSmartWallet(payable(_eoa));
48+
}
49+
50+
// ======== Utility Functions ========
51+
/**
52+
* @dev Creates initialization arguments for CoinbaseSmartWallet
53+
* @param owner Address to set as the initial owner
54+
* @return Encoded initialization arguments for CoinbaseSmartWallet
55+
*/
56+
function _createInitArgs(
57+
address owner
58+
) internal pure returns (bytes memory) {
59+
bytes[] memory owners = new bytes[](1);
60+
owners[0] = abi.encode(owner);
61+
return abi.encode(owners);
62+
}
63+
64+
/**
65+
* @dev Signs initialization data for CoinbaseSmartWallet that will be verified by the proxy
66+
* @param signerPk Private key of the signer
67+
* @param initArgs Initialization arguments to sign
68+
* @return Signature bytes
69+
*/
70+
function _signInitData(
71+
uint256 signerPk,
72+
bytes memory initArgs
73+
) internal view returns (bytes memory) {
74+
bytes32 initHash = keccak256(abi.encode(proxy, initArgs));
75+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, initHash);
76+
return abi.encodePacked(r, s, v);
77+
}
78+
79+
/**
80+
* @dev Helper to create ECDSA signatures
81+
* @param pk Private key to sign with
82+
* @param hash Message hash to sign
83+
* @return signature Encoded signature bytes
84+
*/
85+
function _sign(
86+
uint256 pk,
87+
bytes32 hash
88+
) internal pure returns (bytes memory signature) {
89+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, hash);
90+
return abi.encodePacked(r, s, v);
91+
}
92+
93+
/**
94+
* @dev Creates a signature from a wallet owner for CoinbaseSmartWallet validation
95+
* @param message Message to sign
96+
* @param smartWallet Address of the wallet contract
97+
* @param ownerPk Private key of the owner
98+
* @param ownerIndex Index of the owner in the wallet's owner list
99+
* @return Wrapped signature bytes
100+
*/
101+
function _createOwnerSignature(
102+
bytes32 message,
103+
address smartWallet,
104+
uint256 ownerPk,
105+
uint256 ownerIndex
106+
) internal view returns (bytes memory) {
107+
bytes32 replaySafeHash = CoinbaseSmartWallet(payable(smartWallet))
108+
.replaySafeHash(message);
109+
bytes memory signature = _sign(ownerPk, replaySafeHash);
110+
return _applySignatureWrapper(ownerIndex, signature);
111+
}
112+
113+
/**
114+
* @dev Wraps a signature with owner index for CoinbaseSmartWallet validation
115+
* @param ownerIndex Index of the owner in the wallet's owner list
116+
* @param signatureData Raw signature bytes to wrap
117+
* @return Encoded signature wrapper
118+
*/
119+
function _applySignatureWrapper(
120+
uint256 ownerIndex,
121+
bytes memory signatureData
122+
) internal pure returns (bytes memory) {
123+
return
124+
abi.encode(
125+
CoinbaseSmartWallet.SignatureWrapper(ownerIndex, signatureData)
126+
);
127+
}
128+
129+
// ======== Tests ========
130+
function test_initialize_setsOwner() public {
131+
assertTrue(
132+
wallet.isOwnerAddress(_newOwner),
133+
"New owner should be owner after initialization"
134+
);
135+
}
136+
137+
function test_isValidSignature_succeeds_withValidOwnerSignature(
138+
bytes32 message
139+
) public {
140+
assertTrue(
141+
wallet.isOwnerAddress(_newOwner),
142+
"New owner should be owner after initialization"
143+
);
144+
assertEq(
145+
wallet.ownerAtIndex(0),
146+
abi.encode(_newOwner),
147+
"Owner at index 0 should be new owner"
148+
);
149+
150+
bytes memory signature = _createOwnerSignature(
151+
message,
152+
address(wallet),
153+
_NEW_OWNER_PRIVATE_KEY,
154+
0 // First owner
155+
);
156+
157+
bytes4 result = wallet.isValidSignature(message, signature);
158+
assertEq(
159+
result,
160+
ERC1271_MAGIC_VALUE,
161+
"Should accept valid contract owner signature"
162+
);
163+
}
164+
165+
function test_execute_transfersEth_whenCalledByOwner(
166+
address recipient,
167+
uint256 amount
168+
) public {
169+
vm.assume(recipient != address(0));
170+
assumeNotPrecompile(recipient);
171+
assumePayable(recipient);
172+
vm.assume(amount > 0 && amount <= 100 ether);
173+
174+
vm.deal(address(_eoa), amount);
175+
vm.deal(recipient, 0);
176+
177+
vm.prank(_newOwner);
178+
wallet.execute(
179+
payable(recipient),
180+
amount,
181+
"" // empty calldata for simple transfer
182+
);
183+
184+
assertEq(
185+
recipient.balance,
186+
amount,
187+
"Coinbase wallet execute should transfer ETH"
188+
);
189+
}
190+
191+
function test_upgradeToAndCall_reverts_whenCalledByNonOwner(
192+
address nonOwner
193+
) public {
194+
vm.assume(nonOwner != address(0));
195+
vm.assume(nonOwner != _newOwner); // Ensure caller isn't the actual owner
196+
197+
address newImpl = address(new CoinbaseSmartWallet());
198+
199+
vm.prank(nonOwner);
200+
vm.expectRevert(); // Coinbase wallet specific access control
201+
wallet.upgradeToAndCall(newImpl, "");
202+
}
203+
204+
function test_initialize_reverts_whenCalledTwice() public {
205+
bytes memory initArgs = _createInitArgs(_newOwner);
206+
bytes memory signature = _signInitData(_EOA_PRIVATE_KEY, initArgs);
207+
208+
// Try to initialize again
209+
vm.expectRevert(CoinbaseSmartWallet.Initialized.selector);
210+
EIP7702Proxy(_eoa).initialize(initArgs, signature);
211+
}
212+
}

0 commit comments

Comments
 (0)