Skip to content

Commit

Permalink
Add WebAuthnSignerFactory
Browse files Browse the repository at this point in the history
  • Loading branch information
mmv08 committed Mar 6, 2024
1 parent f03be94 commit c3c3517
Show file tree
Hide file tree
Showing 31 changed files with 4,379 additions and 18,108 deletions.
26 changes: 20 additions & 6 deletions .github/workflows/ci_passkey.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ jobs:
node-version: 20.x
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm run coverage -w modules/passkey
- run: |
npm ci
npm run coverage -w modules/passkey
- uses: coverallsapp/github-action@master
with:
path-to-lcov: modules/passkey/coverage/lcov.info
Expand All @@ -29,7 +30,20 @@ jobs:
node-version: 20.x
cache: npm
cache-dependency-path: package-lock.json
- run: npm ci
- run: npm run lint -w modules/passkey
- run: npm run fmt:check -w modules/passkey
- run: npm run build -w modules/passkey
- run: |
npm ci
npm run lint -w modules/passkey
npm run fmt:check -w modules/passkey
npm run build -w modules/passkey
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 20.x
cache: npm
cache-dependency-path: package-lock.json
- run: |
npm ci
npm run test:e2e -w modules/passkey
4 changes: 2 additions & 2 deletions examples/4337-gas-metering/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"outDir": "./dist" /* Redirect output structure to the directory. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */
},
"exclude": ["node_modules"],
"exclude": ["node_modules"]
}
1 change: 0 additions & 1 deletion modules/4337/contracts/experimental/WebAuthnSigner.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ pragma solidity >=0.8.0;
import {SignatureValidatorConstants} from "./SignatureValidatorConstants.sol";
import {IUniqueSignerFactory} from "./SafeSignerLaunchpad.sol";
import {SignatureValidator} from "./SignatureValidator.sol";
import {SignatureValidatorConstants} from "./SignatureValidatorConstants.sol";
import {IWebAuthnVerifier, WebAuthnConstants} from "./verifiers/WebAuthnVerifier.sol";

struct SignatureData {
Expand Down
8 changes: 5 additions & 3 deletions modules/4337/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
"build"
],
"scripts": {
"build": "hardhat compile",
"build": "npm run build:sol && npm run build:ts",
"build:ts": "npx rimraf dist && tsc",
"build:sol": "npx rimraf build && hardhat compile",
"test": "hardhat test --deploy-fixture",
"test:e2e": "./test/e2e/run.sh",
"test:e2e:upstream": "USE_UPSTREAM_BUNDLER=1 ./test/e2e/run.sh",
Expand All @@ -31,7 +32,8 @@
"fmt": "prettier --write ./contracts/**/*.sol",
"fmt:check": "prettier --check ./**/*.sol",
"prepack": "npm run build",
"prepublish": "npx rimraf build && npm run build && npm run build:ts"
"prepublish": "npm run build",
"postinstall": "npm run build"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -76,6 +78,6 @@
}
},
"dependencies": {
"@safe-global/safe-contracts": "^1.4.1"
"@safe-global/safe-contracts": "^1.4.1-build.0"
}
}
2 changes: 2 additions & 0 deletions modules/passkey/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Entrypoint address to use for the Safe 4337 module deployment in 4337 tests. Default is the canonical EntryPoint v0.7 address
TESTS_4337_MODULE_DEPLOYMENT_ENTRY_POINT_ADDRESS="0x0000000071727De22E5E9d8BAf0edAc6f37da032"
265 changes: 265 additions & 0 deletions modules/passkey/contracts/4337/Safe256BitECSignerLaunchpad.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0 <0.9.0;

import {IAccount} from "@account-abstraction/contracts/interfaces/IAccount.sol";
import {PackedUserOperation} from "@account-abstraction/contracts/interfaces/PackedUserOperation.sol";
import {_packValidationData} from "@account-abstraction/contracts/core/Helpers.sol";
import {SafeStorage} from "@safe-global/safe-contracts/contracts/libraries/SafeStorage.sol";
import {SignatureValidatorConstants} from "../SignatureValidatorConstants.sol";
import {ICustom256BitECSignerFactory} from "../interfaces/ICustomSignerFactory.sol";
import {ISafeSetup} from "../interfaces/ISafe.sol";

/**
* @title SafeOpLaunchpad - A contract for Safe initialization with custom unique signers that would violate ERC-4337 factory rules.
* @dev The is intended to be set as a Safe proxy's implementation for ERC-4337 user operation that deploys the account.
*/
contract Safe256BitECSignerLaunchpad is IAccount, SafeStorage, SignatureValidatorConstants {
bytes32 private constant DOMAIN_SEPARATOR_TYPEHASH = keccak256("EIP712Domain(uint256 chainId,address verifyingContract)");

// keccak256("SafeSignerLaunchpad.initHash") - 1
uint256 private constant INIT_HASH_SLOT = 0x1d2f0b9dbb6ed3f829c9614e6c5d2ea2285238801394dc57e8500e0e306d8f80;

/**
* @notice The keccak256 hash of the EIP-712 SafeInit struct, representing the structure of a ERC-4337 compatible deferred Safe initialization.
* {address} singleton - The singleton to evolve into during the setup.
* {address} signerFactory - The unique signer factory to use for creating an owner.
* {bytes} signerData - The signer data to use the owner.
* {address} setupTo - The contract to delegatecall during setup.
* {bytes} setupData - The calldata for the setup delegatecall.
* {address} fallbackHandler - The fallback handler to initialize the Safe with.
*/
bytes32 private constant SAFE_INIT_TYPEHASH =
keccak256(
"SafeInit(address singleton,address signerFactory,uint256 signerX,uint256 signerY,address signerVerifier,address setupTo,bytes setupData,address fallbackHandler)"
);

/**
* @notice The keccak256 hash of the EIP-712 SafeInitOp struct, representing the user operation to execute alongside initialization.
* {bytes32} userOpHash - The user operation hash being executed.
* {uint48} validAfter - A timestamp representing from when the user operation is valid.
* {uint48} validUntil - A timestamp representing until when the user operation is valid, or 0 to indicated "forever".
* {address} entryPoint - The address of the entry point that will execute the user operation.
*/
bytes32 private constant SAFE_INIT_OP_TYPEHASH =
keccak256("SafeInitOp(bytes32 userOpHash,uint48 validAfter,uint48 validUntil,address entryPoint)");

address private immutable SELF;
address public immutable SUPPORTED_ENTRYPOINT;

constructor(address entryPoint) {
require(entryPoint != address(0), "Invalid entry point");

SELF = address(this);
SUPPORTED_ENTRYPOINT = entryPoint;
}

modifier onlyProxy() {
require(singleton == SELF, "Not called from proxy");
_;
}

modifier onlySupportedEntryPoint() {
require(msg.sender == SUPPORTED_ENTRYPOINT, "Unsupported entry point");
_;
}

receive() external payable {}

function preValidationSetup(bytes32 initHash, address to, bytes calldata preInit) external onlyProxy {
_setInitHash(initHash);
if (to != address(0)) {
(bool success, ) = to.delegatecall(preInit);
require(success, "Pre-initialization failed");
}
}

function getInitHash(
address singleton,
address signerFactory,
uint256 signerX,
uint256 signerY,
address signerVerifier,
address setupTo,
bytes memory setupData,
address fallbackHandler
) public view returns (bytes32 initHash) {
initHash = keccak256(
abi.encodePacked(
bytes1(0x19),
bytes1(0x01),
_domainSeparator(),
keccak256(
abi.encode(
SAFE_INIT_TYPEHASH,
singleton,
signerFactory,
signerX,
signerY,
signerVerifier,
setupTo,
keccak256(setupData),
fallbackHandler
)
)
)
);
}

function getOperationHash(bytes32 userOpHash, uint48 validAfter, uint48 validUntil) public view returns (bytes32 operationHash) {
operationHash = keccak256(_getOperationData(userOpHash, validAfter, validUntil));
}

function validateUserOp(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external override onlyProxy onlySupportedEntryPoint returns (uint256 validationData) {
address signerFactory;
uint256 signerX;
uint256 signerY;
address signerVerifier;
{
require(this.initializeThenUserOp.selector == bytes4(userOp.callData[:4]), "invalid user operation data");

address singleton;
address setupTo;
bytes memory setupData;
address fallbackHandler;
(singleton, signerFactory, signerX, signerY, signerVerifier, setupTo, setupData, fallbackHandler, ) = abi.decode(
userOp.callData[4:],
(address, address, uint256, uint256, address, address, bytes, address, bytes)
);
bytes32 initHash = getInitHash(singleton, signerFactory, signerX, signerY, signerVerifier, setupTo, setupData, fallbackHandler);

require(initHash == _initHash(), "invalid init hash");
}

validationData = _validateSignatures(userOp, userOpHash, signerFactory, signerX, signerY, signerVerifier);
if (missingAccountFunds > 0) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
// The `pop` is necessary here because solidity 0.5.0
// enforces "strict" assembly blocks and "statements (elements of a block) are disallowed if they return something onto the stack at the end."
// This is not well documented, the quote is taken from here:
// https://github.com/ethereum/solidity/issues/1820
// The compiler will throw an error if we keep the success value on the stack
pop(call(gas(), caller(), missingAccountFunds, 0, 0, 0, 0))
}
}
}

/**
* @dev Validates that the user operation is correctly signed and returns an ERC-4337 packed validation data
* of `validAfter || validUntil || authorizer`:
* - `authorizer`: 20-byte address, 0 for valid signature or 1 to mark signature failure (this module does not make use of signature aggregators).
* - `validUntil`: 6-byte timestamp value, or zero for "infinite". The user operation is valid only up to this time.
* - `validAfter`: 6-byte timestamp. The user operation is valid only after this time.
* @param userOp User operation struct.
* @return validationData An integer indicating the result of the validation.
*/
function _validateSignatures(
PackedUserOperation calldata userOp,
bytes32 userOpHash,
address signerFactory,
uint256 signerX,
uint256 signerY,
address signerVerifier
) internal view returns (uint256 validationData) {
uint48 validAfter;
uint48 validUntil;
bytes calldata signature;
{
bytes calldata sig = userOp.signature;
validAfter = uint48(bytes6(sig[0:6]));
validUntil = uint48(bytes6(sig[6:12]));
signature = sig[12:];
}

bytes memory operationData = _getOperationData(userOpHash, validAfter, validUntil);
bytes32 operationHash = keccak256(operationData);
try
ICustom256BitECSignerFactory(signerFactory).isValidSignatureForSigner(
signerX,
signerY,
signerVerifier,
operationHash,
signature
)
returns (bytes4 magicValue) {
// The timestamps are validated by the entry point, therefore we will not check them again
validationData = _packValidationData(magicValue != EIP1271_MAGIC_VALUE, validUntil, validAfter);
} catch {
validationData = _packValidationData(true, validUntil, validAfter);
}
}

function initializeThenUserOp(
address singleton,
address signerFactory,
uint256 signerX,
uint256 signerY,
address signerVerifier,
address setupTo,
bytes calldata setupData,
address fallbackHandler,
bytes memory callData
) external onlySupportedEntryPoint {
SafeStorage.singleton = singleton;
{
address[] memory owners = new address[](1);
owners[0] = ICustom256BitECSignerFactory(signerFactory).createSigner(signerX, signerY, signerVerifier);

ISafeSetup(address(this)).setup(owners, 1, setupTo, setupData, fallbackHandler, address(0), 0, payable(address(0)));
}

(bool success, bytes memory returnData) = address(this).delegatecall(callData);
if (!success) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
revert(add(returnData, 0x20), mload(returnData))
}
}

_setInitHash(0);
}

function _domainSeparator() internal view returns (bytes32) {
return keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, block.chainid, SELF));
}

function _getOperationData(
bytes32 userOpHash,
uint48 validAfter,
uint48 validUntil
) internal view returns (bytes memory operationData) {
operationData = abi.encodePacked(
bytes1(0x19),
bytes1(0x01),
_domainSeparator(),
keccak256(abi.encode(SAFE_INIT_OP_TYPEHASH, userOpHash, validAfter, validUntil, SUPPORTED_ENTRYPOINT))
);
}

function _initHash() public view returns (bytes32 value) {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
value := sload(INIT_HASH_SLOT)
}
}

function _setInitHash(bytes32 value) internal {
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
sstore(INIT_HASH_SLOT, value)
}
}

function _isContract(address account) internal view returns (bool) {
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly ("memory-safe") {
size := extcodesize(account)
}
return size > 0;
}
}
42 changes: 42 additions & 0 deletions modules/passkey/contracts/SignatureValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.8.0;

import {SignatureValidatorConstants} from "./SignatureValidatorConstants.sol";

/**
* @title ISafeSigner
* @dev A interface for smart contract Safe owners that supports multiple `isValidSignature` versions.
*/
abstract contract SignatureValidator is SignatureValidatorConstants {
/**
* @dev Validates the signature for the given data.
* @param data The signed data bytes.
* @param signature The signature to be validated.
* @return magicValue The magic value indicating the validity of the signature.
*/
function isValidSignature(bytes memory data, bytes calldata signature) external view returns (bytes4 magicValue) {
if (_verifySignature(keccak256(data), signature)) {
magicValue = LEGACY_EIP1271_MAGIC_VALUE;
}
}

/**
* @dev Validates the signature for a given data hash.
* @param message The signed message.
* @param signature The signature to be validated.
* @return magicValue The magic value indicating the validity of the signature.
*/
function isValidSignature(bytes32 message, bytes calldata signature) external view returns (bytes4 magicValue) {
if (_verifySignature(message, signature)) {
magicValue = EIP1271_MAGIC_VALUE;
}
}

/**
* @dev Verifies a signature.
* @param message The signed message.
* @param signature The signature to be validated.
* @return isValid Whether or not the signature is valid.
*/
function _verifySignature(bytes32 message, bytes calldata signature) internal view virtual returns (bool isValid);
}
Loading

0 comments on commit c3c3517

Please sign in to comment.