Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to EntryPoint 0.7 #91

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion lib/account-abstraction
39 changes: 21 additions & 18 deletions src/CoinbaseSmartWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ pragma solidity 0.8.23;

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

import {UserOperation, UserOperationLib} from "account-abstraction/interfaces/UserOperation.sol";
import {UserOperationLib} from "account-abstraction/core/UserOperationLib.sol";
import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol";
import {Receiver} from "solady/accounts/Receiver.sol";
import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol";
import {UUPSUpgradeable} from "solady/utils/UUPSUpgradeable.sol";
Expand Down Expand Up @@ -40,11 +41,12 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
bytes data;
}

/// @notice Reserved nonce key (upper 192 bits of `UserOperation.nonce`) for cross-chain replayable
/// @notice Reserved nonce key (upper 192 bits of `PackedUserOperation.nonce`) for cross-chain replayable
/// transactions.
///
/// @dev MUST BE the `UserOperation.nonce` key when `UserOperation.calldata` is calling
/// `executeWithoutChainIdValidation`and MUST NOT BE `UserOperation.nonce` key when `UserOperation.calldata` is
/// @dev MUST BE the `PackedUserOperation.nonce` key when `PackedUserOperation.calldata` is calling
/// `executeWithoutChainIdValidation`and MUST NOT BE `PackedUserOperation.nonce` key when
/// `PackedUserOperation.calldata` is
/// NOT calling `executeWithoutChainIdValidation`.
///
/// @dev Helps enforce sequential sequencing of replayable transactions.
Expand All @@ -59,12 +61,12 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @param selector The selector of the call.
error SelectorNotAllowed(bytes4 selector);

/// @notice Thrown in validateUserOp if the key of `UserOperation.nonce` does not match the calldata.
/// @notice Thrown in validateUserOp if the key of `PackedUserOperation.nonce` does not match the calldata.
///
/// @dev Calls to `this.executeWithoutChainIdValidation` MUST use `REPLAYABLE_NONCE_KEY` and
/// calls NOT to `this.executeWithoutChainIdValidation` MUST NOT use `REPLAYABLE_NONCE_KEY`.
///
/// @param key The invalid `UserOperation.nonce` key.
/// @param key The invalid `PackedUserOperation.nonce` key.
error InvalidNonceKey(uint256 key);

/// @notice Reverts if the caller is not the EntryPoint.
Expand Down Expand Up @@ -130,23 +132,24 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @inheritdoc IAccount
///
/// @notice ERC-4337 `validateUserOp` method. The EntryPoint will
/// call `UserOperation.sender.call(UserOperation.callData)` only if this validation call returns
/// successfully.
/// call `PackedUserOperation.sender.call(PackedUserOperation.callData)` only if this validation call
/// returns successfully.
///
/// @dev Signature failure should be reported by returning 1 (see: `this._isValidSignature`). This
/// allows making a "simulation call" without a valid signature. Other failures (e.g. invalid signature format)
/// should still revert to signal failure.
/// @dev Reverts if the `UserOperation.nonce` key is invalid for `UserOperation.calldata`.
/// @dev Reverts if the `PackedUserOperation.nonce` key is invalid for `PackedUserOperation.callData`.
/// @dev Reverts if the signature format is incorrect or invalid for owner type.
///
/// @param userOp The `UserOperation` to validate.
/// @param userOpHash The `UserOperation` hash, as computed by `EntryPoint.getUserOpHash(UserOperation)`.
/// @param userOp The `PackedUserOperation` to validate.
/// @param userOpHash The `PackedUserOperation` hash, as computed by
/// `EntryPoint.getUserOpHash(PackedUserOperation)`.
/// @param missingAccountFunds The missing account funds that must be deposited on the Entrypoint.
///
/// @return validationData The encoded `ValidationData` structure:
/// `(uint256(validAfter) << (160 + 48)) | (uint256(validUntil) << 160) | (success ? 0 : 1)`
/// where `validUntil` is 0 (indefinite) and `validAfter` is 0.
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds)
external
virtual
onlyEntryPoint
Expand Down Expand Up @@ -180,7 +183,7 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @dev Can only be called by the Entrypoint.
/// @dev Reverts if the given call is not authorized to skip the chain ID validtion.
/// @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
/// it if the `PackedUserOperation.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
/// useful for syncing owner changes.
///
Expand Down Expand Up @@ -228,18 +231,18 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
///
/// @return The address of the EntryPoint v0.6
function entryPoint() public view virtual returns (address) {
return 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789;
return 0x0000000071727De22E5E9d8BAf0edAc6f37da032;
}

/// @notice Computes the hash of the `UserOperation` in the same way as EntryPoint v0.6, but
/// @notice Computes the hash of the `PackedUserOperation` in the same way as EntryPoint v0.7, but
/// leaves out the chain ID.
///
/// @dev This allows accounts to sign a hash that can be used on many chains.
///
/// @param userOp The `UserOperation` to compute the hash for.
/// @param userOp The `PackedUserOperation` to compute the hash for.
///
/// @return The `UserOperation` hash, which does not depend on chain ID.
function getUserOpHashWithoutChainId(UserOperation calldata userOp) public view virtual returns (bytes32) {
/// @return The `PackedUserOperation` hash, which does not depend on chain ID.
function getUserOpHashWithoutChainId(PackedUserOperation calldata userOp) public view virtual returns (bytes32) {
return keccak256(abi.encode(UserOperationLib.hash(userOp), entryPoint()));
}

Expand Down
2 changes: 1 addition & 1 deletion test/CoinbaseSmartWallet/Execute.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ contract TestExecuteWithoutChainIdValidation is SmartWalletTestBase {

function test_revertsWithReservedNonce() public {
userOpNonce = account.REPLAYABLE_NONCE_KEY() << 64;
UserOperation memory userOp = _getUserOpWithSignature();
PackedUserOperation memory userOp = _getUserOpWithSignature();
vm.expectRevert();
_sendUserOperation(userOp);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ contract TestExecuteWithoutChainIdValidation is SmartWalletTestBase {
account.executeWithoutChainIdValidation(calls);
}

function _sign(UserOperation memory userOp) internal view override returns (bytes memory signature) {
function _sign(PackedUserOperation memory userOp) internal view override returns (bytes memory signature) {
bytes32 toSign = account.getUserOpHashWithoutChainId(userOp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, toSign);
signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(r, s, v)));
Expand Down
22 changes: 10 additions & 12 deletions test/CoinbaseSmartWallet/SmartWalletTestBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,47 @@ contract SmartWalletTestBase is Test {
uint256 passkeyPrivateKey = uint256(0x03d99692017473e2d631945a812607b23269d85721e0f370b8d3e7d29a874fd2);
bytes passkeyOwner =
hex"1c05286fe694493eae33312f2d2e0d0abeda8db76238b7a204be1fb87f54ce4228fef61ef4ac300f631657635c28e59bfb2fe71bce1634c81c65642042f6dc4d";
IEntryPoint entryPoint = IEntryPoint(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789);
IEntryPoint entryPoint = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032);
address bundler = address(uint160(uint256(keccak256(abi.encodePacked("bundler")))));

// userOp values
uint256 userOpNonce;
bytes userOpCalldata;

function setUp() public virtual {
vm.etch(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, Static.ENTRY_POINT_BYTES);
vm.etch(0x0000000071727De22E5E9d8BAf0edAc6f37da032, Static.ENTRY_POINT_BYTES);
account = new MockCoinbaseSmartWallet();
owners.push(abi.encode(signer));
owners.push(passkeyOwner);
account.initialize(owners);
}

function _sendUserOperation(UserOperation memory userOp) internal {
UserOperation[] memory ops = new UserOperation[](1);
function _sendUserOperation(PackedUserOperation memory userOp) internal {
PackedUserOperation[] memory ops = new PackedUserOperation[](1);
ops[0] = userOp;
entryPoint.handleOps(ops, payable(bundler));
}

function _getUserOp() internal view returns (UserOperation memory userOp) {
userOp = UserOperation({
function _getUserOp() internal view returns (PackedUserOperation memory userOp) {
userOp = PackedUserOperation({
sender: address(account),
nonce: userOpNonce,
initCode: "",
callData: userOpCalldata,
callGasLimit: uint256(1_000_000),
verificationGasLimit: uint256(1_000_000),
accountGasLimits: bytes32(abi.encodePacked(uint128(1_000_000), uint128(1_000_000))),
gasFees: bytes32(0),
preVerificationGas: uint256(0),
maxFeePerGas: uint256(0),
maxPriorityFeePerGas: uint256(0),
paymasterAndData: "",
signature: ""
});
}

function _getUserOpWithSignature() internal view returns (UserOperation memory userOp) {
function _getUserOpWithSignature() internal view returns (PackedUserOperation memory userOp) {
userOp = _getUserOp();
userOp.signature = _sign(userOp);
}

function _sign(UserOperation memory userOp) internal view virtual returns (bytes memory signature) {
function _sign(PackedUserOperation memory userOp) internal view virtual returns (bytes memory signature) {
bytes32 toSign = entryPoint.getUserOpHash(userOp);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, toSign);
signature = abi.encodePacked(uint8(0), r, s, v);
Expand Down
4 changes: 2 additions & 2 deletions test/CoinbaseSmartWallet/Static.sol

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions test/CoinbaseSmartWallet/ValidateUserOp.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ contract TestValidateUserOp is SmartWalletTestBase {
vm.etch(account.entryPoint(), address(new MockEntryPoint()).code);
MockEntryPoint ep = MockEntryPoint(payable(account.entryPoint()));

UserOperation memory userOp;
PackedUserOperation memory userOp;
// Success returns 0.
userOp.signature = abi.encode(CoinbaseSmartWallet.SignatureWrapper(0, abi.encodePacked(t.r, t.s, t.v)));
assertEq(ep.validateUserOp(address(account), userOp, t.userOpHash, t.missingAccountFunds), 0);
Expand Down Expand Up @@ -72,14 +72,14 @@ contract TestValidateUserOp is SmartWalletTestBase {
vm.etch(account.entryPoint(), address(new MockEntryPoint()).code);
MockEntryPoint ep = MockEntryPoint(payable(account.entryPoint()));

UserOperation memory userOp;
PackedUserOperation memory userOp;
// Success returns 0.
userOp.signature = sig;
assertEq(ep.validateUserOp(address(account), userOp, t.userOpHash, t.missingAccountFunds), 0);
}

function test_reverts_whenSelectorInvalidForReplayableNonceKey() public {
UserOperation memory userOp;
PackedUserOperation memory userOp;
userOp.nonce = 0;
userOp.callData = abi.encodeWithSelector(CoinbaseSmartWallet.executeWithoutChainIdValidation.selector, "");
vm.startPrank(account.entryPoint());
Expand All @@ -88,7 +88,7 @@ contract TestValidateUserOp is SmartWalletTestBase {
}

function test_reverts_whenReplayableNonceKeyInvalidForSelector() public {
UserOperation memory userOp;
PackedUserOperation memory userOp;
userOp.nonce = account.REPLAYABLE_NONCE_KEY() << 64;
userOp.callData = abi.encodeWithSelector(CoinbaseSmartWallet.execute.selector, "");
vm.startPrank(account.entryPoint());
Expand Down
4 changes: 2 additions & 2 deletions test/mocks/MockEntryPoint.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import {CoinbaseSmartWallet, UserOperation} from "../../src/CoinbaseSmartWallet.sol";
import {CoinbaseSmartWallet, PackedUserOperation} from "../../src/CoinbaseSmartWallet.sol";

contract MockEntryPoint {
mapping(address => uint256) public balanceOf;
Expand All @@ -18,7 +18,7 @@ contract MockEntryPoint {

function validateUserOp(
address account,
UserOperation memory userOp,
PackedUserOperation memory userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) public payable returns (uint256 validationData) {
Expand Down