diff --git a/modules/passkey/contracts/SafeWebAuthnSignerProxyFactory.sol b/modules/passkey/contracts/SafeWebAuthnSignerProxyFactory.sol index b3f016e9..eeef4e38 100644 --- a/modules/passkey/contracts/SafeWebAuthnSignerProxyFactory.sol +++ b/modules/passkey/contracts/SafeWebAuthnSignerProxyFactory.sol @@ -6,7 +6,7 @@ import {IP256Verifier} from "./interfaces/IP256Verifier.sol"; import {ERC1271} from "./libraries/ERC1271.sol"; import {WebAuthn} from "./libraries/WebAuthn.sol"; import {SafeWebAuthnSignerProxy} from "./SafeWebAuthnSignerProxy.sol"; - +import {SafeWebAuthnSignerSingleton} from "./SafeWebAuthnSignerSingleton.sol"; /** * @title WebAuthnSignerFactory * @dev A factory contract for creating and managing WebAuthn signers. @@ -16,7 +16,7 @@ contract SafeWebAuthnSignerProxyFactory is ICustomECDSASignerProxyFactory { * @inheritdoc ICustomECDSASignerProxyFactory */ function getSigner(address singleton, uint256 x, uint256 y, address verifier) public view override returns (address signer) { - bytes32 codeHash = keccak256(abi.encodePacked(type(SafeWebAuthnSignerProxy).creationCode, singleton)); + bytes32 codeHash = keccak256(abi.encodePacked(type(SafeWebAuthnSignerProxy).creationCode, uint256(uint160(singleton)))); bytes32 salt = keccak256(abi.encodePacked(x, y, verifier)); signer = address(uint160(uint256(keccak256(abi.encodePacked(hex"ff", address(this), salt, codeHash))))); } @@ -26,9 +26,11 @@ contract SafeWebAuthnSignerProxyFactory is ICustomECDSASignerProxyFactory { */ function createSigner(address singleton, uint256 x, uint256 y, address verifier) external returns (address signer) { signer = getSigner(singleton, x, y, verifier); + bytes32 salt = keccak256(abi.encodePacked(x, y, verifier)); if (_hasNoCode(signer)) { SafeWebAuthnSignerProxy created = new SafeWebAuthnSignerProxy{salt: salt}(singleton); + SafeWebAuthnSignerSingleton(address(created)).setup(x, y, verifier); require(address(created) == signer); } } diff --git a/modules/passkey/src/deploy/webauthn.ts b/modules/passkey/src/deploy/webauthn.ts index 813f0bf2..028ea425 100644 --- a/modules/passkey/src/deploy/webauthn.ts +++ b/modules/passkey/src/deploy/webauthn.ts @@ -10,6 +10,20 @@ const deploy: DeployFunction = async ({ deployments, getNamedAccounts }) => { log: true, deterministicDeployment: true, }) + + await deploy('SafeWebAuthnSignerProxyFactory', { + from: deployer, + args: [], + log: true, + deterministicDeployment: true, + }) + + await deploy('SafeWebAuthnSignerSingleton', { + from: deployer, + args: [], + log: true, + deterministicDeployment: true, + }) } export default deploy diff --git a/modules/passkey/test/GasBenchmarkingProxy.spec.ts b/modules/passkey/test/GasBenchmarkingProxy.spec.ts new file mode 100644 index 00000000..f804e9d5 --- /dev/null +++ b/modules/passkey/test/GasBenchmarkingProxy.spec.ts @@ -0,0 +1,99 @@ +import { expect } from 'chai' +import { deployments, ethers } from 'hardhat' + +import { WebAuthnCredentials, decodePublicKey, encodeWebAuthnSignature } from './utils/webauthn' +import { IP256Verifier } from '../typechain-types' + +describe('Gas Benchmarking Proxy [@bench]', function () { + const navigator = { + credentials: new WebAuthnCredentials(), + } + const credential = navigator.credentials.create({ + publicKey: { + rp: { + name: 'Safe', + id: 'safe.global', + }, + user: { + id: ethers.getBytes(ethers.id('chucknorris')), + name: 'chucknorris', + displayName: 'Chuck Norris', + }, + challenge: ethers.toBeArray(Date.now()), + pubKeyCredParams: [{ type: 'public-key', alg: -7 }], + }, + }) + + const setupTests = deployments.createFixture(async ({ deployments }) => { + const { DaimoP256Verifier, FCLP256Verifier, SafeWebAuthnSignerProxyFactory, SafeWebAuthnSignerSingleton } = await deployments.fixture() + + const Benchmarker = await ethers.getContractFactory('Benchmarker') + const benchmarker = await Benchmarker.deploy() + + const proxyFactory = await ethers.getContractAt('SafeWebAuthnSignerProxyFactory', SafeWebAuthnSignerProxyFactory.address) + + const DummyP256Verifier = await ethers.getContractFactory('DummyP256Verifier') + const verifiers = { + fcl: await ethers.getContractAt('IP256Verifier', FCLP256Verifier.address), + daimo: await ethers.getContractAt('IP256Verifier', DaimoP256Verifier.address), + dummy: await DummyP256Verifier.deploy(), + } as Record + + const singleton = await ethers.getContractAt('SafeWebAuthnSignerSingleton', SafeWebAuthnSignerSingleton.address) + return { benchmarker, proxyFactory, verifiers, singleton } + }) + + describe('SafeWebAuthnSigner', () => { + it(`Benchmark signer deployment cost`, async function () { + const { benchmarker, proxyFactory, singleton } = await setupTests() + + const { x, y } = decodePublicKey(credential.response) + const verifier = `0x${'ee'.repeat(20)}` + + const [gas] = await benchmarker.call.staticCall( + proxyFactory, + proxyFactory.interface.encodeFunctionData('createSigner', [singleton.target, x, y, verifier]), + ) + + console.log(` ⛽ deployment: ${gas}`) + }) + + for (const [name, key] of [ + ['FreshCryptoLib', 'fcl'], + ['daimo-eth', 'daimo'], + ['Dummy', 'dummy'], + ]) { + it(`Benchmark signer verification cost with ${name} verifier`, async function () { + const { benchmarker, verifiers, proxyFactory, singleton } = await setupTests() + + const challenge = ethers.id('hello world') + const assertion = navigator.credentials.get({ + publicKey: { + challenge: ethers.getBytes(challenge), + rpId: 'safe.global', + allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }], + userVerification: 'required', + }, + }) + + const { x, y } = decodePublicKey(credential.response) + const verifier = verifiers[key] + + const isValidSignatureInterface = new ethers.Interface(['function isValidSignature(bytes32,bytes) external view returns (bytes4)']) + await proxyFactory.createSigner(singleton, x, y, verifier) + const signerProxy = await ethers.getContractAt('SafeWebAuthnSignerProxy', await proxyFactory.getSigner(singleton, x, y, verifier)) + const signature = encodeWebAuthnSignature(assertion.response) + + const [gas, returnData] = await benchmarker.call.staticCall( + signerProxy, + isValidSignatureInterface.encodeFunctionData('isValidSignature(bytes32,bytes)', [challenge, signature]), + ) + + const [magicValue] = ethers.AbiCoder.defaultAbiCoder().decode(['bytes4'], returnData) + expect(magicValue).to.equal('0x1626ba7e') + + console.log(` ⛽ verification (${name}): ${gas}`) + }) + } + }) +})