From fbce8f1bc7d4e5183c291f62e07a30648cc70943 Mon Sep 17 00:00:00 2001 From: Akshay Date: Tue, 23 Apr 2024 13:58:00 +0200 Subject: [PATCH] Create SafeWebAuthnSignerProxy (#370) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #312 Changes in PR: - [x] Create `SafeWebAuthnSignerSingleton`. This contract has no storage, no immutables, and no constants. - [x] Create `SafeWebAuthnSignerProxy` and corresponding proxy factory i.e. `SafeWebAuthnSignerProxyFactory` - Create immutable `singleton` contract when deploying proxy factory - `SafeWebAuthnSignerProxyFactory.isValidSignatureForSigner(...)` forwards to singleton contract instead directly calling `WebAuthn` library - `SafeWebAuthnSignerProxy` uses no storage to avoid 4337 storage access restriction violation. - Add unit test to verify that `SafeWebAuthnSignerProxy` forwards every call to singleton address - [x] Report gas usage metrics - [x] Update github workflow to execute benchmarking tests separately. (This is required to get accurate gas usage numbers) - [x] Update documentation - [x] Replace `SafeWebAuthnSigner` with `SafeWebAuthnSignerProxy`, same for factory contract - [x] Update all tests to use `SafeWebAuthnSignerProxy`. Hence, the large diff in this PR. Benchmark number from this PR: ``` Gas Benchmarking Proxy [@bench] SafeWebAuthnSignerProxy ⛽ deployment: 164012 ✔ Benchmark signer deployment cost (10[38](https://github.com/safe-global/safe-modules/actions/runs/8739938588/job/23982409583?pr=370#step:4:39)ms) ⛽ verification (FreshCryptoLib): 216248 ✔ Benchmark signer verification cost with FreshCryptoLib verifier (201ms) ⛽ verification (daimo-eth): 346151 ✔ Benchmark signer verification cost with daimo-eth verifier (186ms) ⛽ verification (Dummy): 14[40](https://github.com/safe-global/safe-modules/actions/runs/8739938588/job/23982409583?pr=370#step:4:41)2 ✔ Benchmark signer verification cost with Dummy verifier ``` Benchmark number from main: ``` Gas Benchmarking [@bench] SafeWebAuthnSigner ⛽ deployment: 491288 ✔ Benchmark signer deployment cost (654ms) ⛽ verification (FreshCryptoLib): 209630 ✔ Benchmark signer verification cost with FreshCryptoLib verifier (138ms) ⛽ verification (daimo-eth): 336881 ✔ Benchmark signer verification cost with daimo-eth verifier (108ms) ⛽ verification (Dummy): 11266 ✔ Benchmark signer verification cost with Dummy verifier ``` --------- Co-authored-by: Nicholas Rodrigues Lordello --- .github/workflows/ci_passkey.yml | 12 ++++ modules/passkey/README.md | 41 ++++++++---- .../passkey/contracts/SafeWebAuthnSigner.sol | 49 -------------- .../contracts/SafeWebAuthnSignerFactory.sol | 46 ++++++++++--- .../contracts/SafeWebAuthnSignerProxy.sol | 64 +++++++++++++++++++ .../contracts/SafeWebAuthnSignerSingleton.sol | 35 ++++++++++ .../passkey/contracts/libraries/WebAuthn.sol | 1 - modules/passkey/test/4337/WebAuthn.spec.ts | 22 ++++--- .../test/4337/WebAuthnSingletonSigner.spec.ts | 2 +- ...g.spec.ts => GasBenchmarkingProxy.spec.ts} | 4 +- ...pec.ts => SafeWebAuthnSignerProxy.spec.ts} | 41 ++++++++++-- ...=> SafeWebAuthnSignerProxyFactory.spec.ts} | 6 +- .../OffchainPasskeyVerification.spec.ts | 2 +- .../PasskeyCredentialCreation.spec.ts | 2 +- 14 files changed, 231 insertions(+), 96 deletions(-) delete mode 100644 modules/passkey/contracts/SafeWebAuthnSigner.sol create mode 100644 modules/passkey/contracts/SafeWebAuthnSignerProxy.sol create mode 100644 modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol rename modules/passkey/test/{GasBenchmarking.spec.ts => GasBenchmarkingProxy.spec.ts} (97%) rename modules/passkey/test/{SafeWebAuthnSigner.spec.ts => SafeWebAuthnSignerProxy.spec.ts} (77%) rename modules/passkey/test/{SafeWebAuthnSignerFactory.spec.ts => SafeWebAuthnSignerProxyFactory.spec.ts} (97%) diff --git a/.github/workflows/ci_passkey.yml b/.github/workflows/ci_passkey.yml index 882212d31..05968a442 100644 --- a/.github/workflows/ci_passkey.yml +++ b/.github/workflows/ci_passkey.yml @@ -47,3 +47,15 @@ jobs: - run: | npm ci npm run test:4337 -w modules/passkey + benchmark: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: npm + cache-dependency-path: package-lock.json + - run: | + npm ci + npm run bench -w modules/passkey diff --git a/modules/passkey/README.md b/modules/passkey/README.md index 56f485c84..a32fb06d3 100644 --- a/modules/passkey/README.md +++ b/modules/passkey/README.md @@ -2,6 +2,10 @@ This package contains a passkey signature verifier, that can be used as an owner for a Safe, compatible with versions 1.3.0+. +## SafeWebAuthnSignerProxy + +Use of `SafeWebAuthnSignerProxy` provides gas savings compared to the complete bytecode contract for each signer creation. The `SafeWebAuthnSignerProxy` contract is a proxy contract that forwards calls to the `SafeWebAuthnSignerSingleton` contract which is a singleton contract. Both `SafeWebAuthnSignerProxy` and `SafeWebAuthnSignerSingleton` use no storage slots to avoid storage access violations defined in ERC-4337. The details on gas savings can be found in [this PR](https://github.com/safe-global/safe-modules/pull/370). + ## Setup and Execution flow ```mermaid @@ -11,30 +15,33 @@ participant CS as CredentialStore actor B as Bundler participant EP as EntryPoint participant SPF as SafeProxyFactory -participant WASF as WebAuthnSignerFactory +participant SWASPF as SafeWebAuthnSignerFactory participant SP as SafeProxy participant SSL as SafeSignerLaunchpad participant S as Singleton participant M as Module -participant WAV as WebAuthnVerifier +participant SWASS as SafeWebAuthnSignerSingleton +participant WAV as WebAuthn library participant PV as P256Verifier actor T as Target U->>+CS: Create Credential (User calls `create(...)`) CS->>U: Decode public key from the return value -U->>+WASF: Get signer address (signer might not be deployed yet) -WASF->>U: Signer address +U->>+SWASPF: Get signer address (signer might not be deployed yet) +SWASPF->>U: Signer address U->>+B: Submit UserOp payload that deploys SafeProxy address with SafeSignerLaunchpad as singleton in initCode and corresponding call data that calls `initializeThenUserOp(...)` ands sets implementation to Safe Singleton B->>+EP: Submit User Operations EP->>+SP: Validate UserOp SP-->>SSL: Load SignerLaunchpad logic -SSL-->>WASF: Forward validation -WASF-->>WAV: call verifyWebAuthnSignatureAllowMalleability +SSL-->>SWASPF: Forward validation +SWASPF-->>SWASS: call isValidSignature(bytes32,bytes) with x,y values and verifier address added to the call data +SWASS-->>WAV: call verifyWebAuthnSignatureAllowMalleability WAV->>+PV: Verify signature PV->>WAV: Signature verification result -WAV->>WASF: Signature verification result -WASF-->>SSL: Return magic value +WAV->>SWASS: Signature verification result +SWASS->>SWASPF: Signature verification result +SWASPF-->>SSL: Return magic value opt Pay required fee SP->>EP: Perform fee payment end @@ -42,8 +49,8 @@ SP-->>-EP: Validation response EP->>+SP: Execute User Operation with call to `initializeThenUserOp(...)` SP-->>SSL: Load SignerLaunchpad logic -SP->>+WASF: Create Signer -WASF-->>SP: Return owner address +SP->>+SWASPF: Create Signer +SWASPF-->>SP: Return owner address SP->>SP: Setup Safe SP-->>SP: delegatecall with calldata received in `initializeThenUserOp(...)` SP-->>S: Load Safe logic @@ -59,9 +66,9 @@ SP->>+T: Perform transaction end ``` -ERC-4337 outlines specific storage access rules for the validation phase, which limits the deployment of SafeProxy for use with the passkey flow. To navigate this restriction, in the `initCode` of UserOp, a SafeProxy is deployed with SafeSignerLaunchpad as a singleton. The SafeSignerLaunchpad is used to validate the signature of the UserOp. The SafeSignerLaunchpad forwards the signature validation to the WebAuthnVerifier, which in turn forwards the signature validation to the P256Verifier. The P256Verifier is used to validate the signature. In the validation, phase the launchpad stores the Safe's setup hash (owners, threshold, modules, etc) which is then verified during the execution phase. +ERC-4337 outlines specific storage access rules for the validation phase, which limits the deployment of SafeProxy for use with the passkey flow. To navigate this restriction, in the `initCode` of UserOp, a `SafeProxy` is deployed with `SafeSignerLaunchpad` as a singleton. The `SafeSignerLaunchpad` is used to validate the signature of the UserOp. The `SafeSignerLaunchpad` forwards the signature validation to the `SafeWebAuthnSignerSingleton`, which in turn forwards the signature validation to the `WebAuthn` library. `WebAuthn` forwards the call to `P256Verifier`. The `P256Verifier` is used to validate the signature. In the validation, phase the launchpad stores the Safe's setup hash (owners, threshold, modules, etc) which is then verified during the execution phase. -During the execution phase, the implementation of the SafeProxy is set to the Safe Singleton along with the owner as signer contract deployed by SafeSignerLaunchpad. +During the execution phase, the implementation of the `SafeProxy` is set to the Safe Singleton along with the owner as signer contract deployed by SafeSignerLaunchpad. ## Usage @@ -128,10 +135,20 @@ This command will upload the contract source to Etherscan. npx hardhat --network etherscan-verify ``` +### Run benchmark tests + +```bash +npm run bench +``` + ## Security and Liability All contracts are WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +## User stories + +The test cases in [userstories](./test/userstories) directory demonstrates the usage of the passkey module in different scenarios like deploying a Safe account with passkey module enabled, executing a `userOp` with a Safe using Passkey signer, etc. + ## License All smart contracts are released under LGPL-3.0. diff --git a/modules/passkey/contracts/SafeWebAuthnSigner.sol b/modules/passkey/contracts/SafeWebAuthnSigner.sol deleted file mode 100644 index 1e35c24fd..000000000 --- a/modules/passkey/contracts/SafeWebAuthnSigner.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -pragma solidity >=0.8.0; - -import {SignatureValidator} from "./base/SignatureValidator.sol"; -import {P256, WebAuthn} from "./libraries/WebAuthn.sol"; - -/** - * @title WebAuthn Safe Signature Validator - * @dev A Safe signature validator implementation for a WebAuthn P-256 credential. - * @custom:security-contact bounty@safe.global - */ -contract SafeWebAuthnSigner is SignatureValidator { - /** - * @notice The X coordinate of the P-256 public key of the WebAuthn credential. - */ - uint256 public immutable X; - - /** - * @notice The Y coordinate of the P-256 public key of the WebAuthn credential. - */ - uint256 public immutable Y; - - /** - * @notice The P-256 verifiers used for ECDSA signature validation. - */ - P256.Verifiers public immutable VERIFIERS; - - /** - * @dev Constructor function. - * @param x The X coordinate of the P-256 public key of the WebAuthn credential. - * @param y The Y coordinate of the P-256 public key of the WebAuthn credential. - * @param verifiers The P-256 verifiers to use for signature validation. This is the - * concatenation of `uint32(precompile) || address(fallback)` specifying the `precompile` to - * use as well as the `fallback` Solidity P-256 verifier implementation in case the precompile - * is not available. - */ - constructor(uint256 x, uint256 y, P256.Verifiers verifiers) { - X = x; - Y = y; - VERIFIERS = verifiers; - } - - /** - * @inheritdoc SignatureValidator - */ - function _verifySignature(bytes32 message, bytes calldata signature) internal view virtual override returns (bool success) { - success = WebAuthn.verifySignature(message, signature, WebAuthn.USER_VERIFICATION, X, Y, VERIFIERS); - } -} diff --git a/modules/passkey/contracts/SafeWebAuthnSignerFactory.sol b/modules/passkey/contracts/SafeWebAuthnSignerFactory.sol index 93af997c7..4f2b38da5 100644 --- a/modules/passkey/contracts/SafeWebAuthnSignerFactory.sol +++ b/modules/passkey/contracts/SafeWebAuthnSignerFactory.sol @@ -2,21 +2,33 @@ pragma solidity >=0.8.0; import {ISafeSignerFactory} from "./interfaces/ISafeSignerFactory.sol"; -import {ERC1271} from "./libraries/ERC1271.sol"; -import {P256, WebAuthn} from "./libraries/WebAuthn.sol"; -import {SafeWebAuthnSigner} from "./SafeWebAuthnSigner.sol"; +import {SafeWebAuthnSignerProxy} from "./SafeWebAuthnSignerProxy.sol"; +import {SafeWebAuthnSignerSingleton} from "./SafeWebAuthnSignerSingleton.sol"; +import {P256} from "./libraries/P256.sol"; /** - * @title WebAuthnSignerFactory - * @dev A factory contract for creating and managing WebAuthn signers. + * @title SafeWebAuthnSignerFactory + * @dev A factory contract for creating and managing WebAuthn proxy signers. */ contract SafeWebAuthnSignerFactory is ISafeSignerFactory { + SafeWebAuthnSignerSingleton public immutable SINGLETON; + + constructor() { + SINGLETON = new SafeWebAuthnSignerSingleton(); + } + /** * @inheritdoc ISafeSignerFactory */ function getSigner(uint256 x, uint256 y, P256.Verifiers verifiers) public view override returns (address signer) { bytes32 codeHash = keccak256( - abi.encodePacked(type(SafeWebAuthnSigner).creationCode, x, y, uint256(P256.Verifiers.unwrap(verifiers))) + abi.encodePacked( + type(SafeWebAuthnSignerProxy).creationCode, + uint256(uint160(address(SINGLETON))), + x, + y, + uint256(P256.Verifiers.unwrap(verifiers)) + ) ); signer = address(uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(this), bytes32(0), codeHash))))); } @@ -28,8 +40,8 @@ contract SafeWebAuthnSignerFactory is ISafeSignerFactory { signer = getSigner(x, y, verifiers); if (_hasNoCode(signer)) { - SafeWebAuthnSigner created = new SafeWebAuthnSigner{salt: bytes32(0)}(x, y, verifiers); - assert(address(created) == signer); + SafeWebAuthnSignerProxy created = new SafeWebAuthnSignerProxy{salt: bytes32(0)}(address(SINGLETON), x, y, verifiers); + require(address(created) == signer); } } @@ -43,8 +55,22 @@ contract SafeWebAuthnSignerFactory is ISafeSignerFactory { uint256 y, P256.Verifiers verifiers ) external view override returns (bytes4 magicValue) { - if (WebAuthn.verifySignature(message, signature, WebAuthn.USER_VERIFICATION, x, y, verifiers)) { - magicValue = ERC1271.MAGIC_VALUE; + address singleton = address(SINGLETON); + bytes memory data = abi.encodePacked( + abi.encodeWithSignature("isValidSignature(bytes32,bytes)", message, signature), + x, + y, + verifiers + ); + + // 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) { + magicValue := mload(0) + } } } diff --git a/modules/passkey/contracts/SafeWebAuthnSignerProxy.sol b/modules/passkey/contracts/SafeWebAuthnSignerProxy.sol new file mode 100644 index 000000000..023c4bb4e --- /dev/null +++ b/modules/passkey/contracts/SafeWebAuthnSignerProxy.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0; +import {P256} from "./libraries/WebAuthn.sol"; + +/** + * @title WebAuthn Safe Signature Validator + * @dev A proxy contracy that points to Safe signature validator implementation for a WebAuthn P-256 credential. + * @custom:security-contact bounty@safe.global + */ +contract SafeWebAuthnSignerProxy { + /** + * @notice The x coordinate of the P-256 public key of the WebAuthn credential. + */ + uint256 internal immutable _X; + /** + * @notice The y coordinate of the P-256 public key of the WebAuthn credential. + */ + uint256 internal immutable _Y; + /** + * @notice The P-256 verifiers used for ECDSA signature validation. + */ + P256.Verifiers internal immutable _VERIFIERS; + + /** + * @notice The contract address to which proxy contract forwards the call via delegatecall. + */ + address internal immutable _SINGLETON; + + /** + * @notice Creates a new WebAuthn Safe Signer Proxy. + * @param singleton Address of the singleton contract to which the proxy forwards the call via delegatecall. + * @param x The x coordinate of the P-256 public key of the WebAuthn credential. + * @param y The y coordinate of the P-256 public key of the WebAuthn credential. + * @param verifiers The P-256 verifiers used for ECDSA signature validation. + */ + constructor(address singleton, uint256 x, uint256 y, P256.Verifiers verifiers) { + _SINGLETON = singleton; + _X = x; + _Y = y; + _VERIFIERS = verifiers; + } + + /** + * @dev Fallback function forwards all transactions and returns all received return data. + */ + // solhint-disable-next-line no-complex-fallback + fallback() external payable { + bytes memory data = abi.encodePacked(msg.data, _X, _Y, _VERIFIERS); + address singleton = _SINGLETON; + + // solhint-disable-next-line no-inline-assembly + assembly { + let dataSize := mload(data) + let dataLocation := add(data, 0x20) + + let success := delegatecall(gas(), singleton, dataLocation, dataSize, 0, 0) + returndatacopy(0, 0, returndatasize()) + if iszero(success) { + revert(0, returndatasize()) + } + return(0, returndatasize()) + } + } +} diff --git a/modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol b/modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol new file mode 100644 index 000000000..098af02ce --- /dev/null +++ b/modules/passkey/contracts/SafeWebAuthnSignerSingleton.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity >=0.8.0; + +import {SignatureValidator} from "./base/SignatureValidator.sol"; +import {P256, WebAuthn} from "./libraries/WebAuthn.sol"; +/** + * @title WebAuthn Safe Signature Validator Singleton + * @dev A contract that represents a WebAuthn signer. + * @custom:security-contact bounty@safe.global + */ +contract SafeWebAuthnSignerSingleton is SignatureValidator { + /** + * @inheritdoc 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); + } + + /** + * @notice Returns the x coordinate, y coordinate, and P-256 verifiers used for ECDSA signature validation. The values are expected to be passed by the SafeWebAuthnSignerProxy contract in msg.data. + * @return x The x coordinate of the P-256 public key. + * @return y The y coordinate of the P-256 public key. + * @return verifiers The P-256 verifiers. + */ + function getConfiguration() public pure returns (uint256 x, uint256 y, P256.Verifiers verifiers) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + x := calldataload(sub(calldatasize(), 88)) + y := calldataload(sub(calldatasize(), 56)) + verifiers := shr(64, calldataload(sub(calldatasize(), 24))) + } + } +} diff --git a/modules/passkey/contracts/libraries/WebAuthn.sol b/modules/passkey/contracts/libraries/WebAuthn.sol index 05b730def..a4b2d0044 100644 --- a/modules/passkey/contracts/libraries/WebAuthn.sol +++ b/modules/passkey/contracts/libraries/WebAuthn.sol @@ -322,7 +322,6 @@ library WebAuthn { // need to encode the signing message if the expected authenticator flags are missing). // However, ordering things this way helps the Solidity compiler generate meaningfully more // optimal code for the "happy path" when Yul optimizations are turned on. - bytes memory message = encodeSigningMessage(challenge, signature.authenticatorData, signature.clientDataFields); if (checkAuthenticatorFlags(signature.authenticatorData, authenticatorFlags)) { success = verifiers.verifySignatureAllowMalleability(_sha256(message), signature.r, signature.s, x, y); diff --git a/modules/passkey/test/4337/WebAuthn.spec.ts b/modules/passkey/test/4337/WebAuthn.spec.ts index 84d0adc15..ff3d4c9c2 100644 --- a/modules/passkey/test/4337/WebAuthn.spec.ts +++ b/modules/passkey/test/4337/WebAuthn.spec.ts @@ -11,6 +11,7 @@ import { chainId } from '@safe-global/safe-4337/test/utils/encoding' import { Safe4337 } from '@safe-global/safe-4337/src/utils/safe' import { WebAuthnCredentials } from '../../test/utils/webauthnShim' import { decodePublicKey, encodeWebAuthnSignature } from '../../src/utils/webauthn' +import { setCode } from '@nomicfoundation/hardhat-toolbox/network-helpers' describe('Safe4337Module - WebAuthn Owner', () => { const setupTests = deployments.createFixture(async ({ deployments }) => { @@ -34,6 +35,9 @@ describe('Safe4337Module - WebAuthn Owner', () => { const singleton = await ethers.getContractAt(SafeL2.abi, SafeL2.address) const verifier = await ethers.getContractAt('IP256Verifier', FCLP256Verifier.address) const signerFactory = await ethers.getContractAt('SafeWebAuthnSignerFactory', SafeWebAuthnSignerFactory.address) + const precompileAddress = `0x${'00'.repeat(18)}0100` + await setCode(precompileAddress, await ethers.provider.getCode(verifier)) + const verifiers = BigInt(ethers.solidityPacked(['uint32', 'address'], [Number(precompileAddress), verifier.target])) const navigator = { credentials: new WebAuthnCredentials(), @@ -49,15 +53,14 @@ describe('Safe4337Module - WebAuthn Owner', () => { singleton, signerFactory, navigator, - verifier, + verifiers, } }) describe('executeUserOp - new account', () => { it('should execute user operation', async () => { - const { user, proxyFactory, safeModuleSetup, module, entryPoint, signerLaunchpad, singleton, signerFactory, navigator, verifier } = + const { user, proxyFactory, safeModuleSetup, module, entryPoint, signerLaunchpad, singleton, signerFactory, navigator, verifiers } = await setupTests() - const verifierAddress = await verifier.getAddress() const credential = navigator.credentials.create({ publicKey: { @@ -75,14 +78,14 @@ describe('Safe4337Module - WebAuthn Owner', () => { }, }) const publicKey = decodePublicKey(credential.response) - const signerAddress = await signerFactory.getSigner(publicKey.x, publicKey.y, verifierAddress) + const signerAddress = await signerFactory.getSigner(publicKey.x, publicKey.y, verifiers) const safeInit = { singleton: singleton.target, signerFactory: signerFactory.target, signerX: publicKey.x, signerY: publicKey.y, - signerVerifiers: verifierAddress, + signerVerifiers: verifiers, setupTo: safeModuleSetup.target, setupData: safeModuleSetup.interface.encodeFunctionData('enableModules', [[module.target]]), fallbackHandler: module.target, @@ -209,8 +212,7 @@ describe('Safe4337Module - WebAuthn Owner', () => { describe('executeUserOp - existing account', () => { it('should execute user operation', async () => { - const { user, proxyFactory, safeModuleSetup, module, entryPoint, singleton, signerFactory, navigator, verifier } = await setupTests() - const verifierAddress = await verifier.getAddress() + const { user, proxyFactory, safeModuleSetup, module, entryPoint, singleton, signerFactory, navigator, verifiers } = await setupTests() const credential = navigator.credentials.create({ publicKey: { rp: { @@ -227,10 +229,10 @@ describe('Safe4337Module - WebAuthn Owner', () => { }, }) const publicKey = decodePublicKey(credential.response) - await signerFactory.createSigner(publicKey.x, publicKey.y, verifierAddress) + await signerFactory.createSigner(publicKey.x, publicKey.y, verifiers) const signer = await ethers.getContractAt( - 'SafeWebAuthnSigner', - await signerFactory.getSigner(publicKey.x, publicKey.y, verifierAddress), + 'SafeWebAuthnSignerProxy', + await signerFactory.getSigner(publicKey.x, publicKey.y, verifiers), ) const safe = await Safe4337.withSigner(await signer.getAddress(), { diff --git a/modules/passkey/test/4337/WebAuthnSingletonSigner.spec.ts b/modules/passkey/test/4337/WebAuthnSingletonSigner.spec.ts index 4a994d535..bfb39e255 100644 --- a/modules/passkey/test/4337/WebAuthnSingletonSigner.spec.ts +++ b/modules/passkey/test/4337/WebAuthnSingletonSigner.spec.ts @@ -8,7 +8,7 @@ import { buildRpcUserOperationFromSafeUserOperation, } from '@safe-global/safe-4337/src/utils/userOp' import { buildSignatureBytes } from '@safe-global/safe-4337/src/utils/execution' -import { WebAuthnCredentials } from '../../test/utils/webauthnShim' +import { WebAuthnCredentials } from '../utils/webauthnShim' import { decodePublicKey, encodeWebAuthnSignature } from '../../src/utils/webauthn' describe('WebAuthn Singleton Signers [@4337]', () => { diff --git a/modules/passkey/test/GasBenchmarking.spec.ts b/modules/passkey/test/GasBenchmarkingProxy.spec.ts similarity index 97% rename from modules/passkey/test/GasBenchmarking.spec.ts rename to modules/passkey/test/GasBenchmarkingProxy.spec.ts index 65be9ab02..b456faced 100644 --- a/modules/passkey/test/GasBenchmarking.spec.ts +++ b/modules/passkey/test/GasBenchmarkingProxy.spec.ts @@ -44,7 +44,7 @@ describe('Gas Benchmarking [@bench]', function () { return { benchmarker, factory, verifiers } }) - describe('SafeWebAuthnSigner', () => { + describe('SafeWebAuthnSignerProxy', () => { it(`Benchmark signer deployment cost`, async function () { const { benchmarker, factory } = await setupTests() @@ -78,7 +78,7 @@ describe('Gas Benchmarking [@bench]', function () { const verifier = await verifiers[key].getAddress() await factory.createSigner(x, y, verifier) - const signer = await ethers.getContractAt('SafeWebAuthnSigner', await factory.getSigner(x, y, verifier)) + const signer = await ethers.getContractAt('SafeWebAuthnSignerSingleton', await factory.getSigner(x, y, verifier)) const signature = encodeWebAuthnSignature(assertion.response) const [gas, returnData] = await benchmarker.call.staticCall( diff --git a/modules/passkey/test/SafeWebAuthnSigner.spec.ts b/modules/passkey/test/SafeWebAuthnSignerProxy.spec.ts similarity index 77% rename from modules/passkey/test/SafeWebAuthnSigner.spec.ts rename to modules/passkey/test/SafeWebAuthnSignerProxy.spec.ts index 0abf6a81e..feff1d50c 100644 --- a/modules/passkey/test/SafeWebAuthnSigner.spec.ts +++ b/modules/passkey/test/SafeWebAuthnSignerProxy.spec.ts @@ -6,7 +6,7 @@ import * as ERC1271 from './utils/erc1271' import { DUMMY_AUTHENTICATOR_DATA, base64UrlEncode, getSignatureBytes } from '../src/utils/webauthn' import { encodeWebAuthnSigningMessage } from './utils/webauthnShim' -describe('SafeWebAuthnSigner', () => { +describe('SafeWebAuthnSignerProxy', () => { const setupTests = deployments.createFixture(async () => { const x = ethers.id('publicKey.x') const y = ethers.id('publicKey.y') @@ -15,20 +15,47 @@ describe('SafeWebAuthnSigner', () => { const precompileAddress = `0x${'00'.repeat(18)}0100` const mockPrecompile = await ethers.getContractAt('MockContract', precompileAddress) await setCode(precompileAddress, await ethers.provider.getCode(mockVerifier)) - const SafeWebAuthnSigner = await ethers.getContractFactory('SafeWebAuthnSigner') const verifiers = BigInt(ethers.solidityPacked(['uint32', 'address'], [Number(precompileAddress), mockVerifier.target])) - const signer = await SafeWebAuthnSigner.deploy(x, y, verifiers) + const singleton = await (await ethers.getContractFactory('SafeWebAuthnSignerSingleton')).deploy() + const SafeWebAuthnSignerProxy = await ethers.getContractFactory('SafeWebAuthnSignerProxy') + const signer = await ethers.getContractAt( + 'SafeWebAuthnSignerSingleton', + (await SafeWebAuthnSignerProxy.deploy(singleton, x, y, verifiers)).target, + ) return { x, y, mockPrecompile, mockVerifier, verifiers, signer } }) + describe('forward call', function () { + it('Should forward call to singleton with additional information', async () => { + const { x, y, mockVerifier } = await setupTests() + const [sender] = await ethers.getSigners() + const mockSingleton = await ethers.getContractAt('MockContract', await (await ethers.getContractFactory('MockContract')).deploy()) + + const verifiers = ethers.solidityPacked(['uint32', 'address'], [0x0100, mockVerifier.target]) + + const signerProxy = await ethers.getContractAt( + 'MockContract', + await (await ethers.getContractFactory('SafeWebAuthnSignerProxy')).deploy(mockSingleton, x, y, verifiers), + ) + + const callData = ethers.hexlify(ethers.randomBytes(36)) + await signerProxy.givenAnyReturnBool(true) + await sender.sendTransaction({ to: signerProxy.target, value: 0, data: callData }) + + expect(await signerProxy.invocationCount()).to.equal(1) + const data = ethers.solidityPacked(['bytes', 'uint256', 'uint256', 'uint192'], [callData, x, y, verifiers]) + expect(await signerProxy.invocationCountForCalldata(data)).to.equal(1) + }) + }) + describe('constructor', function () { it('Should set immutables', async () => { const { x, y, verifiers, signer } = await setupTests() - - expect(await signer.X()).to.equal(x) - expect(await signer.Y()).to.equal(y) - expect(await signer.VERIFIERS()).to.equal(verifiers) + const [X, Y, VERIFIERS] = await signer.getConfiguration() + expect(X).to.equal(x) + expect(Y).to.equal(y) + expect(VERIFIERS).to.equal(verifiers) }) }) diff --git a/modules/passkey/test/SafeWebAuthnSignerFactory.spec.ts b/modules/passkey/test/SafeWebAuthnSignerProxyFactory.spec.ts similarity index 97% rename from modules/passkey/test/SafeWebAuthnSignerFactory.spec.ts rename to modules/passkey/test/SafeWebAuthnSignerProxyFactory.spec.ts index 62b3a67bf..a53db5361 100644 --- a/modules/passkey/test/SafeWebAuthnSignerFactory.spec.ts +++ b/modules/passkey/test/SafeWebAuthnSignerProxyFactory.spec.ts @@ -9,6 +9,7 @@ import { encodeWebAuthnSigningMessage } from './utils/webauthnShim' describe('SafeWebAuthnSignerFactory', () => { const setupTests = deployments.createFixture(async () => { const { SafeWebAuthnSignerFactory } = await deployments.fixture() + const factory = await ethers.getContractAt('SafeWebAuthnSignerFactory', SafeWebAuthnSignerFactory.address) const MockContract = await ethers.getContractFactory('MockContract') @@ -59,13 +60,14 @@ describe('SafeWebAuthnSignerFactory', () => { it('Should create a signer and return its deterministic address', async () => { const { factory, verifiers } = await setupTests() + const singletonAddress = await factory.SINGLETON() const x = ethers.id('publicKey.x') const y = ethers.id('publicKey.y') const signer = await factory.createSigner.staticCall(x, y, verifiers) - const SafeWebAuthnSigner = await ethers.getContractFactory('SafeWebAuthnSigner') - const { data: initCode } = await SafeWebAuthnSigner.getDeployTransaction(x, y, verifiers) + const SafeWebAuthnSignerProxy = await ethers.getContractFactory('SafeWebAuthnSignerProxy') + const { data: initCode } = await SafeWebAuthnSignerProxy.getDeployTransaction(singletonAddress, x, y, verifiers) expect(signer).to.equal(ethers.getCreate2Address(await factory.getAddress(), ethers.ZeroHash, ethers.keccak256(initCode))) expect(ethers.dataLength(await ethers.provider.getCode(signer))).to.equal(0) diff --git a/modules/passkey/test/userstories/OffchainPasskeyVerification.spec.ts b/modules/passkey/test/userstories/OffchainPasskeyVerification.spec.ts index 960829d6d..7ac17e4eb 100644 --- a/modules/passkey/test/userstories/OffchainPasskeyVerification.spec.ts +++ b/modules/passkey/test/userstories/OffchainPasskeyVerification.spec.ts @@ -52,7 +52,7 @@ describe('Offchain Passkey Signature Verification [@userstory]', () => { const publicKey = decodePublicKey(credential.response) await signerFactory.createSigner(publicKey.x, publicKey.y, verifierAddress) const signerAddress = await signerFactory.getSigner(publicKey.x, publicKey.y, verifierAddress) - const signer = await ethers.getContractAt('SafeWebAuthnSigner', signerAddress) + const signer = await ethers.getContractAt('SafeWebAuthnSignerSingleton', signerAddress) // Deploy Safe with the WebAuthn signer as a single owner. const singletonAddress = await singleton.getAddress() diff --git a/modules/passkey/test/userstories/PasskeyCredentialCreation.spec.ts b/modules/passkey/test/userstories/PasskeyCredentialCreation.spec.ts index f940f010c..54fd7a793 100644 --- a/modules/passkey/test/userstories/PasskeyCredentialCreation.spec.ts +++ b/modules/passkey/test/userstories/PasskeyCredentialCreation.spec.ts @@ -51,7 +51,7 @@ describe('Passkey Credential Creation for Safe Ownership [@userstory]', () => { const publicKey = decodePublicKey(credential.response) await signerFactory.createSigner(publicKey.x, publicKey.y, verifierAddress) const signerAddress = await signerFactory.getSigner(publicKey.x, publicKey.y, verifierAddress) - const signer = await ethers.getContractAt('SafeWebAuthnSigner', signerAddress) + const signer = await ethers.getContractAt('SafeWebAuthnSignerSingleton', signerAddress) // Deploy Safe with the WebAuthn signer as a single owner. const singletonAddress = await singleton.getAddress()