Skip to content

Commit

Permalink
Add User Story for Computing Safe Address
Browse files Browse the repository at this point in the history
  • Loading branch information
nlordell committed Jul 10, 2024
1 parent 37def80 commit 0b80eea
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 1 deletion.
2 changes: 1 addition & 1 deletion modules/passkey/test/libraries/WebAuthn.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('WebAuthn Library', () => {
// a large enough client data and exact gas limits to make this happen is a bit annoying, so
// lets hope for no gas schedule changes :fingers_crossed:.
const longClientDataFields = `"long":"${'a'.repeat(100000)}"`
await expect(webAuthnLib.encodeSigningMessage(ethers.ZeroHash, '0x', longClientDataFields, { gasLimit: 1701001 })).to.be.reverted
await expect(webAuthnLib.encodeSigningMessage(ethers.ZeroHash, '0x', longClientDataFields, { gasLimit: 1699001 })).to.be.reverted
})
})

Expand Down
133 changes: 133 additions & 0 deletions modules/passkey/test/userstories/SafeAddressForPasskey.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { expect } from 'chai'
import { deployments, ethers } from 'hardhat'

import { WebAuthnCredentials } from '../../test/utils/webauthnShim'
import { decodePublicKey } from '../../src/utils/webauthn'

/**
* User story: Find Safe for Passkey
* This user story demonstrates to compute deterministically the address of a Safe for a given
* WebAuthn credential. Note that searching for Safes by owner is not really practical without a
* service (as building Safe owners from Ethereum logs is non-trivial). Instead we show that, given
* a Dapp-specific initial Safe setup with a passkey owner, it is possible to find the Safe address
* corresponding to the passkey.
*/
describe('Safe Address for Passkey [@userstory]', () => {
const setupTests = deployments.createFixture(async ({ deployments }) => {
const { SafeProxyFactory, SafeL2, FCLP256Verifier, SafeWebAuthnSignerFactory, SafeWebAuthnSharedSigner } = await deployments.run()

const safeProxyFactory = await ethers.getContractAt(SafeProxyFactory.abi, SafeProxyFactory.address)
const safeSingleton = await ethers.getContractAt(SafeL2.abi, SafeL2.address)
const signerFactory = await ethers.getContractAt('SafeWebAuthnSignerFactory', SafeWebAuthnSignerFactory.address)
const sharedSigner = await ethers.getContractAt('SafeWebAuthnSharedSigner', SafeWebAuthnSharedSigner.address)
const verifier = await ethers.getContractAt('IP256Verifier', FCLP256Verifier.address)

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 signerConfig = {
...decodePublicKey(credential.response),
verifiers: ethers.solidityPacked(['uint16', 'address'], [0, await verifier.getAddress()]),
}

const deploySafe = async ({ initializer, saltNonce }: { initializer: string; saltNonce: bigint }) => {
const safeAddress = await safeProxyFactory.createProxyWithNonce.staticCall(safeSingleton, initializer, saltNonce)
await safeProxyFactory.createProxyWithNonce(safeSingleton, initializer, saltNonce)
return await ethers.getContractAt(SafeL2.abi, safeAddress)
}

return {
safeSingleton,
safeProxyFactory,
signerFactory,
sharedSigner,
signerConfig,
deploySafe,
}
})

it('should compute the Safe address owned by a WebAuthn proxy signer', async () => {
const { safeSingleton, safeProxyFactory, signerFactory, signerConfig, deploySafe } = await setupTests()

await signerFactory.getSigner(signerConfig.x, signerConfig.y, signerConfig.verifiers)
const signer = await ethers.getContractAt(
'SafeWebAuthnSignerSingleton',
await signerFactory.getSigner(signerConfig.x, signerConfig.y, signerConfig.verifiers),
)

const initializer = safeSingleton.interface.encodeFunctionData('setup', [
[await signer.getAddress()],
1,
ethers.ZeroAddress,
'0x',
ethers.ZeroAddress,
ethers.ZeroAddress,
0,
ethers.ZeroAddress,
])
const saltNonce = 42n

const safe = await deploySafe({ initializer, saltNonce })
const deterministicSafeAddress = ethers.getCreate2Address(
await safeProxyFactory.getAddress(),
ethers.solidityPackedKeccak256(['bytes32', 'uint256'], [ethers.keccak256(initializer), saltNonce]),
ethers.solidityPackedKeccak256(
['bytes', 'bytes'],
[
await safeProxyFactory.proxyCreationCode(),
ethers.AbiCoder.defaultAbiCoder().encode(['address'], [await safeSingleton.getAddress()]),
],
),
)

expect(deterministicSafeAddress).to.equal(await safe.getAddress())
})

it('should compute the Safe address owned by a WebAuthn shared signer', async () => {
const { safeSingleton, safeProxyFactory, sharedSigner, signerConfig, deploySafe } = await setupTests()

const initializer = safeSingleton.interface.encodeFunctionData('setup', [
[await sharedSigner.getAddress()],
1,
await sharedSigner.getAddress(),
sharedSigner.interface.encodeFunctionData('configure', [signerConfig]),
ethers.ZeroAddress,
ethers.ZeroAddress,
0,
ethers.ZeroAddress,
])
const saltNonce = 42n

const safe = await deploySafe({ initializer, saltNonce })
const deterministicSafeAddress = ethers.getCreate2Address(
await safeProxyFactory.getAddress(),
ethers.solidityPackedKeccak256(['bytes32', 'uint256'], [ethers.keccak256(initializer), saltNonce]),
ethers.solidityPackedKeccak256(
['bytes', 'bytes'],
[
await safeProxyFactory.proxyCreationCode(),
ethers.AbiCoder.defaultAbiCoder().encode(['address'], [await safeSingleton.getAddress()]),
],
),
)

expect(deterministicSafeAddress).to.equal(await safe.getAddress())
})
})

0 comments on commit 0b80eea

Please sign in to comment.