Skip to content

Commit

Permalink
♻️ set the first signer in the initialize function
Browse files Browse the repository at this point in the history
Starting from the `addFirstSigner` is removed from the `Account` contract.
The first signer will be set in the `initialize` function directly.
Not all the `authData`` is passed to the `initialize` function, only signer's
data.
  • Loading branch information
qd-qd committed Apr 2, 2024
1 parent 9820078 commit 4bd2dc6
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 368 deletions.
124 changes: 65 additions & 59 deletions src/v1/Account/SmartAccount.sol
Original file line number Diff line number Diff line change
Expand Up @@ -73,31 +73,36 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport,
/// @param _entryPoint The address of the 4337 entrypoint used by this implementation
/// @param _webAuthnVerifier The address of the webauthn library used for verify the webauthn signature
constructor(address _entryPoint, address _webAuthnVerifier) {
// 1. set the immutable variables
entryPointAddress = _entryPoint;
webAuthnVerifierAddress = _webAuthnVerifier;

// prevent the implementation contract from being used directly
// 2. prevent the implementation contract from being used directly
_disableInitializers();
}

/// @notice Called once during the creation of the instance. Initialize the contract with the version 1.
// solhint-disable-next-line no-empty-blocks
function initialize() external reinitializer(1) {
// Address of the factory that initialize the proxy that points to this implementation
// Only the factory will have the ability to set the first signer when nonce==0
/// @notice Called once during the creation of the instance. Set the first signer.
function initialize(
bytes32 credIdHash,
uint256 pubX,
uint256 pubY,
bytes calldata credId
)
external
virtual
reinitializer(1)
{
// 1. address of the factory that initialize the proxy contract
factoryAddress = msg.sender;

// 2. set the first signer
_addWebAuthnSigner(credIdHash, pubX, pubY, credId);
}

// ==============================
// ========= MODIFIER ===========
// ==============================

/// @notice This modifier ensure the caller is the factory that deployed this contract
modifier onlyFactory() {
if (msg.sender != factoryAddress) revert NotTheFactory();
_;
}

/// @notice This modifier ensure the caller is the 4337 entrypoint stored
modifier onlyEntrypoint() {
_requireFromEntryPoint();
Expand Down Expand Up @@ -145,20 +150,6 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport,
return IWebAuthn256r1(webAuthnVerifierAddress);
}

/// @notice Remove an existing Webauthn p256r1.
/// @dev This function can only be called by the account itself. The whole 4337 workflow must be respected
/// @param credIdHash The hash of the credential ID associated to the signer
function removeWebAuthnP256R1Signer(bytes32 credIdHash) external onlySelf {
// 1. get the current public key stored
(uint256 pubkeyX, uint256 pubkeyY) = SignerVaultWebAuthnP256R1.pubkey(credIdHash);

// 2. remove the signer from the vault
SignerVaultWebAuthnP256R1.remove(credIdHash);

// 3. emit the event with the removed signer
emit SignerRemoved(Signature.Type.WEBAUTHN_P256R1, credIdHash, pubkeyX, pubkeyY);
}

/// @notice Extract the signer from the authenticatorData
/// @dev This function is free to be called (!!)
/// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer
Expand All @@ -169,56 +160,68 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport,
function extractSignerFromAuthData(bytes calldata authenticatorData)
public
pure
virtual
returns (bytes memory credId, bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY)
{
(credId, credIdHash, pubkeyX, pubkeyY) = SignerVaultWebAuthnP256R1.extractSignerFromAuthData(authenticatorData);
}

/// @notice Set a new Webauthn p256r1 new signer and emit the expected event. This function
/// can not override an existing signer, use `remnoveWebAuthnP256R1Signer` for this
/// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer
function _addWebAuthnSigner(bytes calldata authenticatorData)
internal
returns (bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY)
{
// 0. verify the UV is set in the authenticatorData
if ((authenticatorData[32] & UV_FLAG_MASK) == 0) revert InvalidSignerAddition();

// 1. extract the signer from the authenticatorData
// @DEV: WHY CANNOT WE USE `bytes memory` in the tuple without specify other fucking types?
bytes memory credId;
(credId, credIdHash, pubkeyX, pubkeyY) = extractSignerFromAuthData(authenticatorData);
/// @notice Remove an existing Webauthn p256r1.
/// @dev This function can only be called by the account itself. The whole 4337 workflow must be respected
/// @param credIdHash The hash of the credential ID associated to the signer
function removeWebAuthnP256R1Signer(bytes32 credIdHash) external virtual onlySelf {
// 1. get the current public key stored
(uint256 pubkeyX, uint256 pubkeyY) = SignerVaultWebAuthnP256R1.pubkey(credIdHash);

// 2. Set the new signer in the vault if the signer does not already exist
SignerVaultWebAuthnP256R1.set(credIdHash, pubkeyX, pubkeyY);
// 2. remove the signer from the vault
SignerVaultWebAuthnP256R1.remove(credIdHash);

// 3. emit the event with the added signer
emit SignerAdded(Signature.Type.WEBAUTHN_P256R1, credId, credIdHash, pubkeyX, pubkeyY);
// 3. emit the event with the removed signer
emit SignerRemoved(Signature.Type.WEBAUTHN_P256R1, credIdHash, pubkeyX, pubkeyY);
}

/// @notice Add a Webauthn p256r1 new signer to the account
/// @dev This function can only be called by the account itself. The whole 4337 workflow must be respected
/// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer
function addWebAuthnP256R1Signer(bytes calldata authenticatorData)
external
virtual
onlySelf
returns (bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY)
returns (bytes32, uint256, uint256, bytes memory)
{
return _addWebAuthnSigner(authenticatorData);
// 1. verify the UV is set in the authenticatorData
if ((authenticatorData[32] & UV_FLAG_MASK) == 0) revert InvalidSignerAddition();

// 2. extract the signer from the authenticatorData
(bytes memory credId, bytes32 credIdHash, uint256 pubX, uint256 pubY) =
extractSignerFromAuthData(authenticatorData);

// 3. set the signer in the vault
_addWebAuthnSigner(credIdHash, pubX, pubY, credId);

// 4. return the signer
return (credIdHash, pubX, pubY, credId);
}

/// @notice Add the first signer to the account. This function is only call once by the factory
/// during the deployment of the account. All the future signers must be added using the
/// `addWebAuthnP256R1Signer` function.
/// @dev This function adds a signer generated using the WebAuthn protocol on the
/// secp256r1 curve. This function can only be called once when the nonce of the account is 0x00.
/// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer
function addFirstSigner(bytes calldata authenticatorData) external onlyFactory {
// 1. check that the nonce is 0x00. The value of the first key is checked here
if (getNonce() != 0) revert InvalidFirstSignerAddition();
/// @notice Set a new Webauthn p256r1 new signer and emit the expected event. This function
/// can not override an existing signer, use `remnoveWebAuthnP256R1Signer` for this
/// @param credIdHash The hash of the credential ID associated to the signer
/// @param pubkeyX The X coordinate of the signer's public key
/// @param pubkeyY The Y coordinate of the signer's public key
function _addWebAuthnSigner(
bytes32 credIdHash,
uint256 pubkeyX,
uint256 pubkeyY,
bytes memory credId
)
internal
virtual
{
// 1. Set the new signer in the vault if the signer does not already exist
SignerVaultWebAuthnP256R1.set(credIdHash, pubkeyX, pubkeyY);

// 2. add account's first signer and emit the signer addition event
_addWebAuthnSigner(authenticatorData);
// 2. emit the event with the added signer
emit SignerAdded(Signature.Type.WEBAUTHN_P256R1, credId, credIdHash, pubkeyX, pubkeyY);
}

/// @notice Return a signer stored in the account using its credIdHash. When storing a signer, the credId
Expand All @@ -245,20 +248,23 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport,
/// @param signature The signature field presents in the userOp.
/// @param initCode The initCode field presents in the userOp. It has been used to create the account
/// @return 0 if the signature is valid, 1 otherwise
// TODO: use transient storage for the expected signer/factory?
function _validateCreationSignature(
bytes calldata signature,
bytes calldata initCode
)
internal
view
virtual
returns (uint256)
{
// 1. check that the nonce is 0x00. The value of the first key is checked here
if (getNonce() != 0) return Signature.State.FAILURE;

// 2. get the address of the factory and check it is the expected one
address accountFactory = address(bytes20(initCode[:20]));
if (accountFactory != factoryAddress) return Signature.State.FAILURE;
address storedFactoryAddress = factoryAddress;
if (accountFactory != storedFactoryAddress) return Signature.State.FAILURE;

// 3. decode the rest of the initCode (skip the first 4 bytes -- function selector)
(bytes memory authenticatorData,) = abi.decode(initCode[24:], (bytes, bytes));
Expand All @@ -272,7 +278,7 @@ contract SmartAccount is Initializable, BaseAccount, SmartAccountTokensSupport,
bytes memory message = abi.encode(Signature.Type.CREATION, authenticatorData, address(this), block.chainid);

// 6. fetch the expected signer from the factory contract
address expectedSigner = AccountFactory(factoryAddress).owner();
address expectedSigner = AccountFactory(storedFactoryAddress).owner();

// 7. Check the signature is valid and revert if it is not
// NOTE: The signature prefix, added manually to identify the signature, is removed before the recovery process
Expand Down
123 changes: 75 additions & 48 deletions src/v1/AccountFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ contract AccountFactory is Initializable, OwnableUpgradeable {
}

// ==============================
// ======== FUNCTIONS ===========
// ===== INTERNAL FUNCTIONS =====
// ==============================

/// @notice This function checks if the signature is signed by the operator (owner)
Expand All @@ -89,85 +89,70 @@ contract AccountFactory is Initializable, OwnableUpgradeable {
return Signature.recover(owner(), message, signature[1:]);
}

/// @notice This function either deploys an account and sets its first signer or returns the address of an existing
/// account based on the parameter given
/// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer
/// @param signature Signature made off-chain by made the operator of the factory (owner). It gates the use of the
/// factory.
/// @return The address of the existing account (either deployed by this function or not)
function createAndInitAccount(
bytes calldata authenticatorData,
bytes calldata signature
function _deployAccount(
bytes32 credIdHash,
uint256 pubX,
uint256 pubY,
bytes memory credId
)
external
virtual
returns (address)
internal
returns (SmartAccount account)
{
// 1. extract the signer from the authenticatorData
(, bytes32 credIdHash, uint256 pubX, uint256 pubY) =
SignerVaultWebAuthnP256R1.extractSignerFromAuthData(authenticatorData);

// 2. get the address of the account if it exists
address accountAddress = getAddress(credIdHash, pubX, pubY);

// 3. check if the account is already deployed and return prematurely if it is
if (accountAddress.code.length > 0) return accountAddress;

// 4. check if the signature is valid
if (_isSignatureLegit(accountAddress, authenticatorData, signature) == false) {
revert InvalidSignature(accountAddress, authenticatorData, signature);
}

// 5. deploy the proxy for the user. During the deployment, the initialize function in the implementation
// is called using the `delegatecall` opcode
SmartAccount account = SmartAccount(
account = SmartAccount(
payable(
new ERC1967Proxy{ salt: calculateSalt(credIdHash, pubX, pubY) }(
accountImplementation, abi.encodeWithSelector(SmartAccount.initialize.selector)
new ERC1967Proxy{ salt: _calculateSalt(credIdHash, pubX, pubY) }(
accountImplementation,
abi.encodeWithSelector(SmartAccount.initialize.selector, credIdHash, pubX, pubY, credId)
)
)
);

// 6. set the initial signer of the account defined in the authenticatorData
account.addFirstSigner(authenticatorData);

// 7. emit the event and return the address of the deployed account
emit AccountCreated(address(account), authenticatorData);
return address(account);
}

/// @notice This function calculates the salt used to deploy the account
/// @dev This function must never be changed (!!)
/// @param credIdHash The hash of the credential ID of the signer
/// @param pubkeyX The x-coordinate of the public key of the signer
/// @param pubkeyY The y-coordinate of the public key of the signer
function calculateSalt(bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) internal pure returns (bytes32) {
function _calculateSalt(bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) internal pure returns (bytes32) {
// 1. encode the signer and hash the result to get the salt
return keccak256(abi.encodePacked(credIdHash, pubkeyX, pubkeyY));
}

/// @notice This utility function returns the address of the account that would be deployed using the salt
/// @dev This is the under the hood formula used by the CREATE2 opcode. This function must never be changed (!!)
/// @param credIdHash The hash of the credential ID of the signer
/// @param pubkeyX The x-coordinate of the public key of the signer
/// @param pubkeyY The y-coordinate of the public key of the signer
/// @param pubX The x-coordinate of the public key of the signer
/// @param pubY The y-coordinate of the public key of the signer
/// @param credId The credential ID of the signer
/// @return The address of the account that would be deployed
function getAddress(bytes32 credIdHash, uint256 pubkeyX, uint256 pubkeyY) internal view returns (address) {
function _getAddress(
bytes32 credIdHash,
uint256 pubX,
uint256 pubY,
bytes memory credId
)
internal
view
returns (address)
{
return address(
uint160(
uint256(
keccak256(
abi.encodePacked(
bytes1(0xff), // init code hash prefix
address(this), // deployer address
calculateSalt(credIdHash, pubkeyX, pubkeyY),
_calculateSalt(credIdHash, pubX, pubY),
keccak256( // the init code hash
abi.encodePacked(
// creation code of the contract deployed
type(ERC1967Proxy).creationCode,
// arguments passed to the constructor of the contract deployed
abi.encode(
accountImplementation, abi.encodeWithSelector(SmartAccount.initialize.selector)
accountImplementation,
abi.encodeWithSelector(
SmartAccount.initialize.selector, credIdHash, pubX, pubY, credId
)
)
)
)
Expand All @@ -178,18 +163,60 @@ contract AccountFactory is Initializable, OwnableUpgradeable {
);
}

// ==============================
// ===== EXTERNAL FUNCTIONS =====
// ==============================

/// @notice This function either deploys an account and sets its first signer or returns the address of an existing
/// account based on the parameter given
/// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer
/// @param signature Signature made off-chain by made the operator of the factory (owner). It gates the use of the
/// factory.
/// @return The address of the existing account (either deployed by this function or not)
function createAndInitAccount(
bytes calldata authenticatorData,
bytes calldata signature
)
external
virtual
returns (address)
{
// 1. extract the signer from the authenticatorData
(bytes memory credId, bytes32 credIdHash, uint256 pubX, uint256 pubY) =
SignerVaultWebAuthnP256R1.extractSignerFromAuthData(authenticatorData);

// 2. get the address of the account if it exists
address accountAddress = _getAddress(credIdHash, pubX, pubY, credId);

// 3. check if the account is already deployed and return prematurely if it is
if (accountAddress.code.length > 0) return accountAddress;

// 4. check if the signature is valid
if (_isSignatureLegit(accountAddress, authenticatorData, signature) == false) {
revert InvalidSignature(accountAddress, authenticatorData, signature);
}

// 5. deploy the proxy for the user. During the deployment, the initialize function in the implementation
// is called using the `delegatecall` opcode
SmartAccount account = _deployAccount(credIdHash, pubX, pubY, credId);

// 6. emit the event and return the address of the deployed account
emit AccountCreated(address(account), authenticatorData);
return address(account);
}

/// @notice This utility function returns the address of the account that would be deployed using the authData
/// @dev The salt is calculated using the signer extracted from the authenticatorData. This function must never
/// be changed (!!)
/// @param authenticatorData The authenticatorData field of the WebAuthn response when creating a signer
/// @return The address of the account that would be deployed
function getAddress(bytes calldata authenticatorData) external view returns (address) {
// 1. extract the signer from the authenticatorData
(, bytes32 credIdHash, uint256 pubX, uint256 pubY) =
(bytes memory credId, bytes32 credIdHash, uint256 pubX, uint256 pubY) =
SignerVaultWebAuthnP256R1.extractSignerFromAuthData(authenticatorData);

// 2. return the address of the account that would be deployed
return getAddress(credIdHash, pubX, pubY);
return _getAddress(credIdHash, pubX, pubY, credId);
}

function version() external pure virtual returns (uint256) {
Expand Down
Loading

0 comments on commit 4bd2dc6

Please sign in to comment.