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
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: 256, μ: 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: 256, μ: 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: 256, μ: 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: 256, μ: 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: 256, μ: 3640157, ~: 3767417)
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: 423221)
TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsBadOwnerIndex() (gas: 35642)
TestIsValidSignature:testValidateSignatureWithPasskeySignerFailsWithWrongBadSignature() (gas: 430687)
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, μ: 39328, ~: 39348)
CoinbaseSmartWalletFactoryTest:test_constructor_setsImplementation(address) (runs: 256, μ: 455545, ~: 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, μ: 4507288, ~: 4347762)
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, μ: 23179, ~: 23228)
TestValidateUserOp:test_succeedsWithEOASigner() (gas: 456302)
TestValidateUserOp:test_succeedsWithPasskeySigner() (gas: 711374)
TestValidateUserOp:test_succeeds_whenUpgradeToImplementationWithCode() (gas: 3714211)
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,19 @@ Today, allowed are
- MultiOwnable.removeOwnerAtIndex
- UUPSUpgradeable.upgradeToAndCall

> [!WARNING]
> ## EIP-7702 delegation safety
> Do **not** directly delegate to a Coinbase Smart Wallet **implementation** contract via EIP-7702. Delegating directly to an implementation can create a security vulnerability.
>
> Instead:
> - Use the `EIP7702Proxy` pattern: [base/eip-7702-proxy](https://github.com/base/eip-7702-proxy)
> - See Coinbase Developer Platform docs for further guidance and an example of performing a safe EIP-7702 upgrade: [EIP-7702 FAQs – safe upgrade](https://docs.cdp.coinbase.com/paymaster/need-to-knows/eip-7702-faqs#how-do-i-upgrade-my-wallet-to-7702)

## Deployments
Factory and implementation are deployed via [Safe Singleton Factory](https://github.com/safe-global/safe-singleton-factory), which today will give the same address across 248 chains. See "Deploying" below for instructions on how to deploy to new chains.
| Version | Factory Address |
|-----------|-----------------------------------------|
| 1.1 | [0xBA5ED110eFDBa3D005bfC882d75358ACBbB85842](https://basescan.org/address/0xBA5ED110eFDBa3D005bfC882d75358ACBbB85842) |
| 1 | [0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a](https://basescan.org/address/0x0BA5ED0c6AA8c49038F819E587E2633c4A9F428a) |


Expand Down
Binary file added audits/Cantina-April-2024.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions audits/Code4rena-March-2024.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[View report.](https://code4rena.com/reports/2024-03-coinbase)
26 changes: 26 additions & 0 deletions foundry.lock
Original file line number Diff line number Diff line change
@@ -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": "5ea5d9f57ed6d24a27d00934f4a3448def931415"
},
"lib/webauthn-sol": {
"rev": "619f20ab0f074fef41066ee4ab24849a913263b2"
}
}
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
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 55 files
+1 −0 .github/CODEOWNERS
+57 −109 .github/workflows/ci.yml
+3 −1 .github/workflows/sync.yml
+0 −3 .gitmodules
+193 −0 CONTRIBUTING.md
+21 −5 README.md
+5 −3 foundry.toml
+0 −1 lib/ds-test
+1 −1 package.json
+13 −2 scripts/vm.py
+16 −9 src/Base.sol
+1 −0 src/Script.sol
+590 −202 src/StdAssertions.sol
+49 −11 src/StdChains.sol
+17 −5 src/StdCheats.sol
+30 −0 src/StdConstants.sol
+18 −3 src/StdInvariant.sol
+111 −11 src/StdJson.sol
+202 −107 src/StdStorage.sol
+283 −0 src/StdToml.sol
+1 −19 src/StdUtils.sol
+5 −4 src/Test.sol
+1,416 −62 src/Vm.sol
+635 −608 src/console.sol
+1 −1,555 src/console2.sol
+1 −1 src/interfaces/IERC1155.sol
+3 −3 src/interfaces/IERC4626.sol
+72 −0 src/interfaces/IERC6909.sol
+1 −1 src/interfaces/IERC721.sol
+150 −0 src/interfaces/IERC7540.sol
+241 −0 src/interfaces/IERC7575.sol
+0 −216 src/mocks/MockERC20.sol
+0 −221 src/mocks/MockERC721.sol
+693 −4 src/safeconsole.sol
+44 −0 test/CommonBase.t.sol
+35 −909 test/StdAssertions.t.sol
+55 −44 test/StdChains.t.sol
+66 −37 test/StdCheats.t.sol
+38 −0 test/StdConstants.t.sol
+12 −12 test/StdError.t.sol
+49 −0 test/StdJson.t.sol
+12 −22 test/StdMath.t.sol
+190 −17 test/StdStorage.t.sol
+1 −1 test/StdStyle.t.sol
+49 −0 test/StdToml.t.sol
+26 −26 test/StdUtils.t.sol
+9 −6 test/Vm.t.sol
+1 −1 test/compilation/CompilationScript.sol
+1 −1 test/compilation/CompilationScriptBase.sol
+1 −1 test/compilation/CompilationTest.sol
+1 −1 test/compilation/CompilationTestBase.sol
+8 −0 test/fixtures/test.json
+6 −0 test/fixtures/test.toml
+0 −441 test/mocks/MockERC20.t.sol
+0 −721 test/mocks/MockERC721.t.sol
11 changes: 11 additions & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@openzeppelin/contracts/=lib/openzeppelin-contracts/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/
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
forge-std/=lib/forge-std/src/
openzeppelin-contracts/=lib/openzeppelin-contracts/
p256-verifier/=lib/p256-verifier/
safe-singleton-deployer-sol/=lib/safe-singleton-deployer-sol/
solady/=lib/solady/src/
webauthn-sol/=lib/webauthn-sol/src/
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
6 changes: 6 additions & 0 deletions snapshots/EndToEndTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"e2e_transfer_erc20_baseAccount": "142302",
"e2e_transfer_erc20_eoa": "39910",
"e2e_transfer_native_baseAccount": "134635",
"e2e_transfer_native_eoa": "9338"
}
23 changes: 22 additions & 1 deletion 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 All @@ -178,7 +199,7 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @notice Executes `calls` on this account (i.e. self call).
///
/// @dev Can only be called by the Entrypoint.
/// @dev Reverts if the given call is not authorized to skip the chain ID validtion.
/// @dev Reverts if the given call is not authorized to skip the chain ID validation.
/// @dev `validateUserOp()` will recompute the `userOpHash` without the chain ID before validating
/// it if the `UserOperation.calldata` is calling this function. This allows certain UserOperations
/// to be replayed for all accounts sharing the same address across chains. E.g. This may be
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
Loading