Skip to content

Commit

Permalink
Optimize WebAuthn Signer Proxy Implementation (#406)
Browse files Browse the repository at this point in the history
This PR uses assembly to encode the calldata for calling the WebAuthn
signer singleton instead of using native Solidity. This has notable
improvements to the deployment gas costs:

**Before**:

```
    SafeWebAuthnSignerProxy
      ⛽ deployment: 107899
      ✔ Benchmark signer deployment cost (867ms)
```

**After**:

```
    SafeWebAuthnSignerProxy
      ⛽ deployment: 92481
      ✔ Benchmark signer deployment cost (849ms)
```

This is a result of reduced codesize and not significantly more
performant code.

---

I also cleaned up some comments in this PR.
  • Loading branch information
nlordell authored May 10, 2024
1 parent 213b84f commit f5b9f22
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 21 deletions.
18 changes: 8 additions & 10 deletions modules/passkey/contracts/SafeWebAuthnSignerFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {SafeWebAuthnSignerSingleton} from "./SafeWebAuthnSignerSingleton.sol";
import {P256} from "./libraries/P256.sol";

/**
* @title SafeWebAuthnSignerFactory
* @dev A factory contract for creating and managing WebAuthn proxy signers.
* @title Safe WebAuthn Signer Factory
* @dev A factory contract for creating WebAuthn signers. Additionally, the factory supports
* signature verification without deploying a signer proxies.
* @custom:security-contact bounty@safe.global
*/
contract SafeWebAuthnSignerFactory is ISafeSignerFactory {
/**
Expand Down Expand Up @@ -76,11 +78,9 @@ contract SafeWebAuthnSignerFactory is ISafeSignerFactory {

// solhint-disable-next-line no-inline-assembly
assembly {
let dataSize := mload(data)
let dataLocation := add(data, 0x20)
// staticcall to the singleton contract with return size given as 32 bytes. The
// singleton contract is known and immutable so it is safe to specify return size.
if staticcall(gas(), singleton, dataLocation, dataSize, 0, 32) {
if staticcall(gas(), singleton, add(data, 0x20), mload(data), 0, 32) {
magicValue := mload(0)
}
}
Expand All @@ -89,14 +89,12 @@ contract SafeWebAuthnSignerFactory is ISafeSignerFactory {
/**
* @dev Checks if the provided account has no code.
* @param account The address of the account to check.
* @return True if the account has no code, false otherwise.
* @return result True if the account has no code, false otherwise.
*/
function _hasNoCode(address account) internal view returns (bool) {
uint256 size;
function _hasNoCode(address account) internal view returns (bool result) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
size := extcodesize(account)
result := iszero(extcodesize(account))
}
return size == 0;
}
}
24 changes: 18 additions & 6 deletions modules/passkey/contracts/SafeWebAuthnSignerProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import {P256} from "./libraries/WebAuthn.sol";

/**
* @title Safe WebAuthn Signer Proxy
* @dev A proxy to a {SafeWebAuthnSignerSingleton} signature validator implementation for Safe
* accounts. Using a proxy pattern for the signature validator greatly reduces deployment gas costs.
* @dev A specialized proxy to a {SafeWebAuthnSignerSingleton} signature validator implementation
* for Safe accounts. Using a proxy pattern for the signature validator greatly reduces deployment
* gas costs.
* @custom:security-contact bounty@safe.global
*/
contract SafeWebAuthnSignerProxy {
Expand Down Expand Up @@ -49,15 +50,26 @@ contract SafeWebAuthnSignerProxy {
* @dev Fallback function forwards all transactions and returns all received return data.
*/
fallback() external payable {
bytes memory data = abi.encodePacked(msg.data, _X, _Y, _VERIFIERS);
address singleton = _SINGLETON;
uint256 x = _X;
uint256 y = _Y;
P256.Verifiers verifiers = _VERIFIERS;

// solhint-disable-next-line no-inline-assembly
assembly {
let dataSize := mload(data)
let dataLocation := add(data, 0x20)
// Forward the call to the singleton implementation. We append the configuration to the
// calldata instead of having the singleton implementation read it from storage. This is
// both more gas efficient and required for ERC-4337 compatibility. Note that we append
// the configuration fields in reverse order since the fields are packed, and this makes
// it so we don't need to mask any bits from the `verifiers` value. This computes `data`
// to be `abi.encodePacked(msg.data, x, y, verifiers)`.
let data := mload(0x40)
mstore(add(data, add(calldatasize(), 0x36)), verifiers)
mstore(add(data, add(calldatasize(), 0x20)), y)
mstore(add(data, calldatasize()), x)
calldatacopy(data, 0x00, calldatasize())

let success := delegatecall(gas(), singleton, dataLocation, dataSize, 0, 0)
let success := delegatecall(gas(), singleton, data, add(calldatasize(), 0x56), 0, 0)
returndatacopy(0, 0, returndatasize())
if iszero(success) {
revert(0, returndatasize())
Expand Down
10 changes: 5 additions & 5 deletions modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {SignatureValidator} from "./base/SignatureValidator.sol";
import {P256, WebAuthn} from "./libraries/WebAuthn.sol";

/**
* @title WebAuthn Safe Signature Validator Singleton
* @dev A singleton contract that implements WebAuthn signature verification. This sigleton contract
* must be used with the {SafeWebAuthnSignerProxy}, as it encodes the credential configuration
* (public key coordinates and P-256 verifier to use) that is required by this implementation.
* @title Safe WebAuthn Signer Singleton
* @dev A singleton contract that implements WebAuthn signature verification. This singleton
* contract must be used with the specialized proxy {SafeWebAuthnSignerProxy}, as it encodes the
* credential configuration (public key coordinates and P-256 verifier to use) in calldata, which is
* required by this implementation.
* @custom:security-contact bounty@safe.global
*/
contract SafeWebAuthnSignerSingleton is SignatureValidator {
Expand All @@ -17,7 +18,6 @@ contract SafeWebAuthnSignerSingleton is SignatureValidator {
*/
function _verifySignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool success) {
(uint256 x, uint256 y, P256.Verifiers verifiers) = getConfiguration();

success = WebAuthn.verifySignature(message, signature, WebAuthn.USER_VERIFICATION, x, y, verifiers);
}

Expand Down
19 changes: 19 additions & 0 deletions modules/passkey/contracts/interfaces/ISafe.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;

/**
* @title Safe Smart Account
* @dev Minimal interface of a Safe smart account. This only includes functions that are used by
* this project.
* @custom:security-contact bounty@safe.global
*/
interface ISafe {
/**
* @notice Sets an initial storage of the Safe contract.
* @dev This method can only be called once. If a proxy was created without setting up, anyone
* can call setup and claim the proxy.
* @param _owners List of Safe owners.
* @param _threshold Number of required confirmations for a Safe transaction.
* @param to Contract address for optional delegate call.
* @param data Data payload for optional delegate call.
* @param fallbackHandler Handler for fallback calls to this contract
* @param paymentToken Token that should be used for the payment (0 is ETH)
* @param payment Value that should be paid
* @param paymentReceiver Address that should receive the payment (or 0 if tx.origin)
*/
function setup(
address[] calldata _owners,
uint256 _threshold,
Expand Down

0 comments on commit f5b9f22

Please sign in to comment.