diff --git a/.gas-snapshot b/.gas-snapshot index 106e732..782c3af 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -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) \ No newline at end of file +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 diff --git a/foundry.toml b/foundry.toml index 2de3e9f..0846d5e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 diff --git a/script/DeployFactory.s.sol b/script/DeployFactory.s.sol index 2cb9963..d798d18 100644 --- a/script/DeployFactory.s.sol +++ b/script/DeployFactory.s.sol @@ -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); diff --git a/src/CoinbaseSmartWallet.sol b/src/CoinbaseSmartWallet.sol index 652ac83..a3b02fd 100644 --- a/src/CoinbaseSmartWallet.sol +++ b/src/CoinbaseSmartWallet.sol @@ -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()) { @@ -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); diff --git a/src/CoinbaseSmartWalletFactory.sol b/src/CoinbaseSmartWalletFactory.sol index 7d7c770..1be6049 100644 --- a/src/CoinbaseSmartWalletFactory.sol +++ b/src/CoinbaseSmartWalletFactory.sol @@ -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(); @@ -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_; } @@ -52,6 +63,7 @@ contract CoinbaseSmartWalletFactory { account = CoinbaseSmartWallet(payable(accountAddress)); if (!alreadyDeployed) { + emit AccountCreated(address(account), owners, nonce); account.initialize(owners); } } diff --git a/test/CoinbaseSmartWallet/ValidateUserOp.t.sol b/test/CoinbaseSmartWallet/ValidateUserOp.t.sol index 446c5cc..026729f 100644 --- a/test/CoinbaseSmartWallet/ValidateUserOp.t.sol +++ b/test/CoinbaseSmartWallet/ValidateUserOp.t.sol @@ -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); + } } diff --git a/test/CoinbaseSmartWalletFactory.t.sol b/test/CoinbaseSmartWalletFactory.t.sol index ee906ad..b144442 100644 --- a/test/CoinbaseSmartWalletFactory.t.sol +++ b/test/CoinbaseSmartWalletFactory.t.sol @@ -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); } @@ -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();