Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 76 additions & 72 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,72 +1,76 @@
AddOwnerAddressTest:testEmitsAddOwner() (gas: 92424)
AddOwnerAddressTest:testIncreasesOwnerIndex() (gas: 90698)
AddOwnerAddressTest:testRevertsIfAlreadyOwner() (gas: 93235)
AddOwnerAddressTest:testRevertsIfCalledByNonOwner() (gas: 11786)
AddOwnerAddressTest:testSetsIsOwner() (gas: 90492)
AddOwnerAddressTest:testSetsOwnerAtIndex() (gas: 98992)
AddOwnerPublicKeyTest:testEmitsAddOwner() (gas: 115559)
AddOwnerPublicKeyTest:testFuzzIsOwnerPublicKey(bytes32,bytes32) (runs: 257, μ: 115372, ~: 115372)
AddOwnerPublicKeyTest:testRevertsIfAlreadyOwner() (gas: 116399)
AddOwnerPublicKeyTest:testRevertsIfCalledByNonOwner() (gas: 11776)
AddOwnerPublicKeyTest:testSetsIsOwner() (gas: 113813)
AddOwnerPublicKeyTest:testSetsOwnerAtIndex() (gas: 128638)
CoinbaseSmartWallet1271InputGeneratorTest:testGetReplaySafeHashForDeployedAccount() (gas: 308479)
CoinbaseSmartWallet1271InputGeneratorTest:testGetReplaySafeHashForUndeployedAccount() (gas: 291508)
CoinbaseSmartWalletFactoryTest:testDeployDeterministicPassValues() (gas: 267283)
CoinbaseSmartWalletFactoryTest:test_CreateAccount_ReturnsPredeterminedAddress_WhenAccountAlreadyExists() (gas: 286935)
CoinbaseSmartWalletFactoryTest:test_RevertsIfLength32ButLargerThanAddress() (gas: 298669)
CoinbaseSmartWalletFactoryTest:test_constructor_setsImplementation(address) (runs: 257, μ: 313355, ~: 313355)
CoinbaseSmartWalletFactoryTest:test_createAccountDeploysToPredeterminedAddress() (gas: 268634)
CoinbaseSmartWalletFactoryTest:test_createAccountSetsOwnersCorrectly() (gas: 277324)
CoinbaseSmartWalletFactoryTest:test_exitIfAccountIsAlreadyInitialized() (gas: 267739)
CoinbaseSmartWalletFactoryTest:test_implementation_returnsExpectedAddress() (gas: 7676)
CoinbaseSmartWalletFactoryTest:test_initCodeHash() (gas: 7912)
CoinbaseSmartWalletFactoryTest:test_revertsIfNoOwners() (gas: 29232)
ERC1271Test:test_returnsExpectedDomainHashWhenProxy() (gas: 29243)
ERC1271Test:test_static() (gas: 3247131)
MultiOwnableInitializeTest:testRevertsIfLength32ButLargerThanAddress() (gas: 81111)
MultiOwnableInitializeTest:testRevertsIfLength32NotAddress() (gas: 81092)
MultiOwnableInitializeTest:testRevertsIfLengthNot32Or64() (gas: 103395)
RemoveLastOwnerTest:test_emitsRemoveOwner() (gas: 50363)
RemoveLastOwnerTest:test_removesOwner() (gas: 49657)
RemoveLastOwnerTest:test_removesOwnerAtIndex() (gas: 49588)
RemoveLastOwnerTest:test_revert_whenCalledByNonOwner(address) (runs: 257, μ: 19094, ~: 19094)
RemoveLastOwnerTest:test_revert_whenNoOwnerAtIndex() (gas: 48407)
RemoveLastOwnerTest:test_revert_whenWrongOwnerAtIndex() (gas: 34181)
RemoveLastOwnerTest:test_reverts_whenNotLastOwner() (gas: 123462)
RemoveOwnerAtIndexTest:test_emitsRemoveOwner() (gas: 55549)
RemoveOwnerAtIndexTest:test_removesOwner() (gas: 54828)
RemoveOwnerAtIndexTest:test_removesOwnerAtIndex() (gas: 54566)
RemoveOwnerAtIndexTest:test_revert_whenCalledByNonOwner(address) (runs: 257, μ: 19145, ~: 19145)
RemoveOwnerAtIndexTest:test_revert_whenNoOwnerAtIndex() (gas: 33406)
RemoveOwnerAtIndexTest:test_revert_whenWrongOwnerAtIndex() (gas: 36728)
RemoveOwnerAtIndexTest:test_reverts_ifIsLastOwner() (gas: 7647492)
TestCanSkipChainIdValidation:test_approvedSelectorsReturnTrue() (gas: 17781)
TestCanSkipChainIdValidation:test_otherSelectorsReturnFalse() (gas: 12579)
TestExecuteWithoutChainIdValidation:testExecute() (gas: 403873)
TestExecuteWithoutChainIdValidation:testExecuteBatch() (gas: 728942)
TestExecuteWithoutChainIdValidation:testExecuteBatch(uint256) (runs: 257, μ: 3628778, ~: 3584759)
TestExecuteWithoutChainIdValidation:test__codesize() (gas: 50211)
TestExecuteWithoutChainIdValidation:test_revertsWithReservedNonce() (gas: 81941)
TestExecuteWithoutChainIdValidation:test_reverts_whenCallerNotEntryPoint() (gas: 11031)
TestExecuteWithoutChainIdValidation:test_reverts_whenOneCallReverts() (gas: 467783)
TestExecuteWithoutChainIdValidation:test_reverts_whenOneSelectorNotApproved() (gas: 179662)
TestExecuteWithoutChainIdValidation:test_reverts_whenSelectorNotApproved() (gas: 106736)
TestExecuteWithoutChainIdValidation:test_succeeds_whenSelectorAllowed() (gas: 423963)
TestImplementation:testImplementation() (gas: 12558)
TestInitialize:testInitialize() (gas: 21012)
TestInitialize:test_cannotInitImplementation() (gas: 2896342)
TestIsValidSignature:testReturnsInvalidIfPasskeySigButWrongOwnerLength() (gas: 40165)
TestIsValidSignature:testRevertsIfEthereumSignatureButWrongOwnerLength() (gas: 24018)
TestIsValidSignature:testRevertsIfOwnerIsInvalidEthereumAddress() (gas: 21999)
TestIsValidSignature:testSmartWalletSigner() (gas: 3177948)
TestIsValidSignature:testValidateSignatureWithEOASigner() (gas: 24900)
TestIsValidSignature:testValidateSignatureWithEOASignerFailsWithWrongSigner() (gas: 23855)
TestIsValidSignature:testValidateSignatureWithPasskeySigner() (gas: 437729)
TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsBadOwnerIndex() (gas: 35642)
TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsWithWrongBadSignature() (gas: 428169)
TestUpgradeToAndCall:testUpgradeToAndCall() (gas: 25499)
TestValidateUserOp:test_reverts_whenReplayableNonceKeyInvalidForSelector() (gas: 14211)
TestValidateUserOp:test_reverts_whenSelectorInvalidForReplayableNonceKey() (gas: 14476)
TestValidateUserOp:test_succeedsWithEOASigner() (gas: 448010)
TestValidateUserOp:test_succeedsWithPasskeySigner() (gas: 785198)
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)
3 changes: 3 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ libs = ["lib"]
[profile.deploy]
optimizer = true
optimizer_runs = 999999
via_ir = true
evm_version = "prague"
solc_version = "0.8.23"

[fmt]
sort_imports = true
Expand Down
9 changes: 5 additions & 4 deletions script/DeployFactory.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ import {SafeSingletonDeployer} from "safe-singleton-deployer-sol/src/SafeSinglet
import {CoinbaseSmartWallet, CoinbaseSmartWalletFactory} from "../src/CoinbaseSmartWalletFactory.sol";

contract DeployFactoryScript is Script {
address constant EXPECTED_IMPLEMENTATION = 0x000100abaad02f1cfC8Bbe32bD5a564817339E72;
address constant EXPECTED_FACTORY = 0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a;
address constant EXPECTED_IMPLEMENTATION = 0x00000110dCdEdC9581cb5eCB8467282f2926534d;
address constant EXPECTED_FACTORY = 0xBA5ED110eFDBa3D005bfC882d75358ACBbB85842;

function run() public {
console2.log("Deploying on chain ID", block.chainid);
address implementation = SafeSingletonDeployer.broadcastDeploy({
creationCode: type(CoinbaseSmartWallet).creationCode,
salt: 0x3438ae5ce1ff7750c1e09c4b28e2a04525da412f91561eb5b57729977f591fbb
salt: 0x3771220e68256b8d5aa359fe953bf594dad1a5473239d1251256f0e5e7473b16
});
console2.log("implementation", implementation);
assert(implementation == EXPECTED_IMPLEMENTATION);

address factory = SafeSingletonDeployer.broadcastDeploy({
creationCode: type(CoinbaseSmartWalletFactory).creationCode,
args: abi.encode(EXPECTED_IMPLEMENTATION),
salt: 0x278d06dab87f67bb2d83470a70c8975a2c99872f290058fb43bcc47da5f0390c
salt: 0x0000000000000000000000000000000000000000e8448b6b950698874d6a35bd
});
console2.log("factory", factory);
assert(factory == EXPECTED_FACTORY);
Expand Down
21 changes: 21 additions & 0 deletions src/CoinbaseSmartWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @param key The invalid `UserOperation.nonce` key.
error InvalidNonceKey(uint256 key);

/// @notice Thrown when an upgrade is attempted to an implementation that does not exist.
///
/// @param implementation The address of the implementation that has no code.
error InvalidImplementation(address implementation);

/// @notice Reverts if the caller is not the EntryPoint.
modifier onlyEntryPoint() virtual {
if (msg.sender != entryPoint()) {
Expand Down Expand Up @@ -160,6 +165,22 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
if (key != REPLAYABLE_NONCE_KEY) {
revert InvalidNonceKey(key);
}

// Check for upgrade calls in the batch and validate implementation has code
bytes[] memory calls = abi.decode(userOp.callData[4:], (bytes[]));
for (uint256 i; i < calls.length; i++) {
bytes memory callData = calls[i];
bytes4 selector = bytes4(callData);

if (selector == UUPSUpgradeable.upgradeToAndCall.selector) {
address newImplementation;
assembly {
// Skip reading the first 32 bytes (length prefix) + 4 bytes (function selector)
newImplementation := mload(add(callData, 36))
}
if (newImplementation.code.length == 0) revert InvalidImplementation(newImplementation);
}
}
} else {
if (key == REPLAYABLE_NONCE_KEY) {
revert InvalidNonceKey(key);
Expand Down
12 changes: 12 additions & 0 deletions src/CoinbaseSmartWalletFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ contract CoinbaseSmartWalletFactory {
/// @notice Address of the ERC-4337 implementation used as implementation for new accounts.
address public immutable implementation;

/// @notice Emitted when a new account is created.
///
/// @param account The address of the created account.
/// @param owners Array of initial owners.
/// @param nonce The nonce of the created account.
event AccountCreated(address indexed account, bytes[] owners, uint256 nonce);

/// @notice Thrown when trying to construct with an implementation that is not deployed.
error ImplementationUndeployed();

/// @notice Thrown when trying to create a new `CoinbaseSmartWallet` account without any owner.
error OwnerRequired();

Expand All @@ -22,6 +32,7 @@ contract CoinbaseSmartWalletFactory {
///
/// @param implementation_ The address of the CoinbaseSmartWallet implementation which new accounts will proxy to.
constructor(address implementation_) payable {
if (implementation_.code.length == 0) revert ImplementationUndeployed();
implementation = implementation_;
}

Expand Down Expand Up @@ -52,6 +63,7 @@ contract CoinbaseSmartWalletFactory {
account = CoinbaseSmartWallet(payable(accountAddress));

if (!alreadyDeployed) {
emit AccountCreated(address(account), owners, nonce);
account.initialize(owners);
}
}
Expand Down
38 changes: 38 additions & 0 deletions test/CoinbaseSmartWallet/ValidateUserOp.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,42 @@ contract TestValidateUserOp is SmartWalletTestBase {
);
account.validateUserOp(userOp, "", 0);
}

function test_reverts_whenUpgradeToImplementationWithNoCode(address emptyImplementation) public {
vm.assume(emptyImplementation.code.length == 0);

// Create a UserOperation that calls executeWithoutChainIdValidation with an upgrade call
bytes[] memory calls = new bytes[](1);
calls[0] = abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, emptyImplementation, "");

UserOperation memory userOp;
userOp.nonce = account.REPLAYABLE_NONCE_KEY() << 64;
userOp.callData = abi.encodeWithSelector(CoinbaseSmartWallet.executeWithoutChainIdValidation.selector, calls);
userOp.signature =
abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(bytes32(0), bytes32(0), uint8(27))));

vm.startPrank(account.entryPoint());
vm.expectRevert(abi.encodeWithSelector(CoinbaseSmartWallet.InvalidImplementation.selector, emptyImplementation));
account.validateUserOp(userOp, "", 0);
}

function test_succeeds_whenUpgradeToImplementationWithCode() public {
// Deploy a mock implementation that has code
MockCoinbaseSmartWallet mockImpl = new MockCoinbaseSmartWallet();
address validImplementation = address(mockImpl);

// Create a UserOperation that calls executeWithoutChainIdValidation with an upgrade call
bytes[] memory calls = new bytes[](1);
calls[0] = abi.encodeWithSelector(UUPSUpgradeable.upgradeToAndCall.selector, validImplementation, "");

UserOperation memory userOp;
userOp.nonce = account.REPLAYABLE_NONCE_KEY() << 64;
userOp.callData = abi.encodeWithSelector(CoinbaseSmartWallet.executeWithoutChainIdValidation.selector, calls);
userOp.signature =
abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(bytes32(0), bytes32(0), uint8(27))));

vm.startPrank(account.entryPoint());
// Should revert with signature error (1) rather than InvalidImplementation
assertEq(account.validateUserOp(userOp, "", 0), 1);
}
}
21 changes: 21 additions & 0 deletions test/CoinbaseSmartWalletFactory.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,21 @@ contract CoinbaseSmartWalletFactoryTest is Test {
owners.push(abi.encode(address(2)));
}

function test_constructor_revertsIfImplementationIsNotDeployed(address implementation) public {
vm.assume(implementation.code.length == 0);
vm.expectRevert(CoinbaseSmartWalletFactory.ImplementationUndeployed.selector);
new CoinbaseSmartWalletFactory(implementation);
}

function test_constructor_setsImplementation(address implementation) public {
// avoid precompiles in fuzz runs
vm.assume(uint160(implementation) > 100);

// set bytecode if not already set
if (implementation.code.length == 0) {
vm.etch(implementation, address(account).code);
}

factory = new CoinbaseSmartWalletFactory(implementation);
assertEq(factory.implementation(), implementation);
}
Expand All @@ -32,6 +46,13 @@ contract CoinbaseSmartWalletFactoryTest is Test {
assert(a.isOwnerAddress(address(2)));
}

function test_createAccount_emitsAccountCreatedEvent(uint256 nonce) public {
address expectedAddress = factory.getAddress(owners, nonce);
vm.expectEmit(true, true, true, true);
emit CoinbaseSmartWalletFactory.AccountCreated(expectedAddress, owners, nonce);
factory.createAccount(owners, nonce);
}

function test_revertsIfNoOwners() public {
owners.pop();
owners.pop();
Expand Down
Loading