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
4 changes: 3 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
url = https://github.com/openzeppelin/openzeppelin-contracts
[submodule "lib/webauthn-sol"]
path = lib/webauthn-sol
url = https://github.com/base-org/webauthn-sol
url = https://github.com/amiecorso/webauthn-sol
[submodule "lib/safe-singleton-deployer-sol"]
path = lib/safe-singleton-deployer-sol
url = https://github.com/wilsoncusack/safe-singleton-deployer-sol
[submodule "webauthn-sol"]
url = https://github.com/amiecorso/webauthn-sol
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,42 @@
# Smart Wallet

## Branch note: Simulation bytecode for accurate Verification Gas Limit (VGL) estimation (not for merge)

- **Purpose**: Provide simulation-only wallet bytecode that mimics the onchain “valid signature” verification path, so bundlers can estimate Verification Gas Limit (VGL) accurately without manual buffers.
- **Why**: Gas estimation in simulation usually uses invalid passkey signatures against the production wallet bytecode. Invalid signatures trigger different execution paths than real valid signatures onchain (e.g., falling back to FCL instead of using RIP-7212), leading to large deviations in measured gas. This branch supplies bytecode that fakes the “valid signature” path by hard-coding a known‑valid P‑256 vector inside the verifier, ensuring simulation follows the same path as real execution and yields sufficiently accurate VGL for bundler overrides.
- **Scope**: Simulation-only. Not intended for deployment. This branch will not be merged.

### What changed (high level)
- `CoinbaseSmartWallet._isValidSignature` calls `WebAuthn.verifySim`, whose internal signature check uses a fixed valid vector to exercise the RIP-7212 precompile path when available (and FCL fallback otherwise). This produces gas that matches real executions when a valid passkey signature is used onchain.

### Build settings used for bytecode (must match deploy settings)
- Foundry profile: `deploy`
- `optimizer = true`
- `optimizer_runs = 999999`
- `via_ir = true`
- `evm_version = "prague"`
- `solc_version = "0.8.23"`

Build and extract the deployed/runtime bytecode:

```bash
FOUNDRY_PROFILE=deploy forge build
FOUNDRY_PROFILE=deploy forge inspect src/CoinbaseSmartWallet.sol:CoinbaseSmartWallet deployedBytecode \
> snapshots/SimulationOverrides/CoinbaseSmartWallet.runtime.hex
```

- **Output artifact location**: `snapshots/SimulationOverrides/CoinbaseSmartWallet.runtime.hex` (hex-encoded runtime bytecode, prefixed with `0x`).

### Using this in a bundler (simulation overrides)
During `eth_estimateUserOperationGas` for passkey flows:
- Override `CoinbaseSmartWallet` implementation address(es) with the bytecode from `snapshots/SimulationOverrides/CoinbaseSmartWallet.runtime.hex`.
- Provide a dummy signature so calldata and control flow match production, but let `verifySim` ensure the signature path succeeds.

### Notes
- On RIP-7212 chains, simulation follows the precompile success path. On non-7212 chains, it follows FCL. This mirrors real execution and stabilizes VGL estimation across chains.
- WebAuthn library commit pinned in this branch: `amiecorso/webauthn-sol@6ac7461cbb768d77d9798e6160cd05d70dee7586`.


# Smart Wallet

This repository contains code for a new, [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) compliant smart contract wallet from Coinbase.
Expand Down

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the output we need to pass as bundler override to eth_estimateUserOperationGas

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions src/CoinbaseSmartWallet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @dev Reverts if the `UserOperation.nonce` key is invalid for `UserOperation.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 `UserOperation` to validate.
/// @param userOpHash The `UserOperation` hash, as computed by `EntryPoint.getUserOpHash(UserOperation)`.
/// @param missingAccountFunds The missing account funds that must be deposited on the Entrypoint.
///
/// @return validationData The encoded `ValidationData` structure:
Expand Down Expand Up @@ -223,8 +223,8 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @dev Can only be called by the Entrypoint or an owner of this account (including itself).
///
/// @param target The address to call.
/// @param value The value to send with the call.
/// @param data The data of the call.
/// @param value The value to send with the call.
/// @param data The data of the call.
function execute(address target, uint256 value, bytes calldata data)
external
payable
Expand Down Expand Up @@ -296,10 +296,9 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable
/// @dev Reverts if the call reverted.
/// @dev Implementation taken from
/// https://github.com/alchemyplatform/light-account/blob/43f625afdda544d5e5af9c370c9f4be0943e4e90/src/common/BaseLightAccount.sol#L125
///
/// @param target The target call address.
/// @param value The call value to user.
/// @param data The raw call data.
/// @param value The call value to user.
/// @param data The raw call data.
function _call(address target, uint256 value, bytes memory data) internal {
(bool success, bytes memory result) = target.call{value: value}(data);
if (!success) {
Expand Down Expand Up @@ -339,7 +338,7 @@ contract CoinbaseSmartWallet is ERC1271, IAccount, MultiOwnable, UUPSUpgradeable

WebAuthn.WebAuthnAuth memory auth = abi.decode(sigWrapper.signatureData, (WebAuthn.WebAuthnAuth));

return WebAuthn.verify({challenge: abi.encode(hash), requireUV: false, webAuthnAuth: auth, x: x, y: y});
return WebAuthn.verifySim(abi.encode(hash), false, auth, x, y);
}

revert InvalidOwnerBytesLength(ownerBytes);
Expand Down
38 changes: 34 additions & 4 deletions test/CoinbaseSmartWallet/IsValidSignature.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "./SmartWalletTestBase.sol";
import "webauthn-sol/../test/Utils.sol";

contract TestIsValidSignature is SmartWalletTestBase {
bytes4 constant EIP1271_MAGICVALUE = 0x1626ba7e;
bytes4 constant EIP1271_FAILVALUE = 0xffffffff;
function testValidateSignatureWithPasskeySigner() public {
bytes32 hash = 0x15fa6f8c855db1dccbb8a42eef3a7b83f11d29758e84aed37312527165d5eec5;
bytes32 challenge = account.replaySafeHash(hash);
Expand All @@ -30,7 +32,7 @@ contract TestIsValidSignature is SmartWalletTestBase {

// check a valid signature
bytes4 ret = account.isValidSignature(hash, sig);
assertEq(ret, bytes4(0x1626ba7e));
assertEq(ret, EIP1271_MAGICVALUE);
}

function testSmartWalletSigner() public {
Expand Down Expand Up @@ -86,7 +88,7 @@ contract TestIsValidSignature is SmartWalletTestBase {
account.isValidSignature(hash, sig);
}

function testValidateSignatureWithPasskeySignerFailsWithWrongBadSignature() public {
function testValidateSignatureWithPasskeySignerInvalidSigNowSucceedsWithVerifySim() public {
bytes32 hash = 0x15fa6f8c855db1dccbb8a42eef3a7b83f11d29758e84aed37312527165d5eec5;
bytes32 challenge = account.replaySafeHash(hash);
WebAuthnInfo memory webAuthn = Utils.getWebAuthnStruct(challenge);
Expand All @@ -110,9 +112,37 @@ contract TestIsValidSignature is SmartWalletTestBase {
})
);

// check a valid signature
// With verifySim in use, signature contents are ignored for the final check.
// Even though r is intentionally wrong, this should still return MAGICVALUE.
bytes4 ret = account.isValidSignature(hash, sig);
assertEq(ret, bytes4(0xffffffff));
assertEq(ret, EIP1271_MAGICVALUE);
}

function testInvalidPasskeySignatureStillSucceedsWithVerifySim() public {
bytes32 hash = 0x15fa6f8c855db1dccbb8a42eef3a7b83f11d29758e84aed37312527165d5eec5;
bytes32 challenge = account.replaySafeHash(hash);
WebAuthnInfo memory webAuthn = Utils.getWebAuthnStruct(challenge);

// Construct a clearly invalid signature vector (zeroed r,s after normalization edge avoided).
// Even with invalid r,s, verifySim should return MAGICVALUE.
bytes memory sig = abi.encode(
CoinbaseSmartWallet.SignatureWrapper({
ownerIndex: 1,
signatureData: abi.encode(
WebAuthn.WebAuthnAuth({
authenticatorData: webAuthn.authenticatorData,
clientDataJSON: webAuthn.clientDataJSON,
typeIndex: 1,
challengeIndex: 23,
r: uint256(0),
s: uint256(0)
})
)
})
);

bytes4 ret = account.isValidSignature(hash, sig);
assertEq(ret, EIP1271_MAGICVALUE);
}

function testValidateSignatureWithEOASigner() public {
Expand Down
Loading