ERC-6492 Signature verification for contracts before deployment (Signature Validation for Predeploy Contracts)
Final · Signatures & auth · created 2023-02-10
Makes it possible to verify signatures created by a contract wallet that has not been deployed yet. When deployment information is appended to the signature, the verifier deploys the contract first if needed and then continues with ERC-1271 verification.
Why it is needed
As account abstraction wallets have spread, contract wallets often choose to deploy the contract only when the user sends their first transaction. This spares users a separate deployment step before they can use the wallet. However, many apps require signatures not only for transactions but also for login. A signature created by a contract is verified by asking that contract directly under ERC-1271, but if the contract has not been deployed yet, there is nothing to ask, so verification is impossible. As a result, a contract wallet's ability to sign messages has been limited until the actual deployment happens. The inability of an undeployed contract to sign has also long been a limitation of ERC-1271.
What to implement
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4 magicValue);: The function defined by ERC-1271. It takes a hash and a signature and returns the magic value 0x1626ba7e if the signature is valid.error ERC1271Revert(bytes error);: In the reference implementation, an error that carries the error data back when the ERC-1271 verification call reverts.error ERC6492DeployFailed(bytes error);: In the reference implementation, an error that carries the error data back when the factory call (deployment or preparation transaction) fails.function isValidSigWithSideEffects(address _signer, bytes32 _hash, bytes calldata _signature) external returns (bool): Verifies with allowSideEffects set to true. It is not reentrancy-safe, but in some cases it costs less gas.function isValidSig(address _signer, bytes32 _hash, bytes calldata _signature) external returns (bool): A reentrancy-safe verification function. If there are deployment side effects, it receives the result through revert data and rolls back the state changes.constructor (address _signer, bytes32 _hash, bytes memory _signature): The constructor of ValidateSigOffchain. To finish verification in a single eth_call, it creates the validator and returns a single byte as the result.
In depth
Wrapped signature format and magicBytes
A contract before deployment wraps its signature in the form `concat(abi.encode((create2Factory, factoryCalldata, originalERC1271Signature), (address, bytes, bytes)), magicBytes)`. magicBytes MUST be set to `0x6492649264926492649264926492649264926492649264926492649264926492`, and the verifier detects the wrapped format by checking whether the signature ends with this value. When the wrapped format is detected, the verifier MUST deploy the contract before calling isValidSignature. The reason for passing factoryCalldata instead of a salt and bytecode is to fit any factory interface. ERC-1271 verification already assumes the account address is known, so there is no need to recompute the address from create2Factory, salt, and bytecode. It is RECOMMENDED to use this with CREATE2 contracts whose deployment address is always predictable.
Three cases on the signer side
The signing contract is usually a contract wallet, but it can be any contract that implements ERC-1271 and is deployed counterfactually. First, if it is already deployed, it creates an ordinary ERC-1271 signature. Second, if it is not yet deployed, it wraps the signature with create2Factory and factoryCalldata. Third, if it is deployed but not ready for ERC-1271 verification, it wraps the signature as `concat(abi.encode((prepareTo, prepareData, originalERC1271Signature), (address, bytes, bytes)), magicBytes)`. In this case prepareTo and prepareData MUST contain the transaction that puts the contract into a state where ERC-1271 verification works (for example, a migrate or update call).
Required order on the verifier side
The full verification MUST follow this order. 1) If the signature ends with magicBytes, send an eth_call to a multicall contract that first calls the factory with factoryCalldata to deploy the contract if it does not exist, and then calls isValidSignature with the unwrapped signature. 2) If the address has contract code, perform ERC-1271 verification with isValidSignature as usual. 3) If ERC-1271 verification failed and the factory deployment call was skipped because the wallet already had code, execute the factoryCalldata transaction and then try isValidSignature again. 4) If the address has no contract code, attempt ecrecover verification.
Why the order is set this way
The magicBytes check MUST come before the ordinary ERC-1271 check. That way, a signature created before deployment stays valid after the contract is deployed. The magicBytes check MUST also come before ecrecover, to prevent attempting to verify with ecrecover something that is clearly a pre-deployment contract signature. ecrecover MUST NOT come before ERC-1271 verification. This is because the signature format a contract uses may happen to also be valid as an externally owned account (EOA) signature for a different address. A wallet contract controlled by such an EOA is one example. If the wallet already has code but the deployment prefix is attached, it cannot be determined whether the signature was made before deployment or the wallet is not ready, so both possibilities are tried. magicBytes ends with `0x92` so that it does not collide with an ecrecover signature in r, s, v format. `0x92` is not a valid v value. It was also made bytes32 in length to avoid collisions with ordinary ERC-1271 signatures.
Reference implementation UniversalSigValidator
The reference implementation is a universal validation contract that works both on-chain and off-chain, and it assumes a singleton deployment. It verifies signatures under this standard, ERC-1271, and traditional ecrecover. It validates the final digest (_hash), so EIP-712 is supported as well. isValidSigImpl decides whether a signature is counterfactual by checking whether the last 32 bytes of the signature equal ERC6492_DETECTION_SUFFIX. If the contract code length is 0 or tryPrepare is true, it executes create2Factory.call(factoryCalldata). If that fails, it reverts with ERC6492DeployFailed. It then checks whether the isValidSignature result is ERC1271_SUCCESS(0x1626ba7e). If the check failed or the call reverted while code exists, it sets tryPrepare to true and tries once more. In the ecrecover step, the signature length must be 65 bytes and v may only be 27 or 28.
Two ways to validate on-chain
Two functions are available on-chain. isValidSig(_signer, _hash, _signature) returns a bool indicating whether the signature is valid, and it is reentrancy-safe. Internally it calls isValidSigImpl with allowSideEffects set to false. When deployment has changed state, it receives the result as one byte of revert data and thereby rolls back the state change. Revert data of length 1 is interpreted as the result, and any other error is passed through as is. isValidSigWithSideEffects(_signer, _hash, _signature) produces the same result but is not reentrancy-safe, in exchange for better gas efficiency in some cases. With both functions, if an inner call reverts, they may be reverted as well.
Off-chain validation takes a single eth_call
ValidateSigOffchain creates a new UniversalSigValidator inside its constructor, calls isValidSigWithSideEffects, and returns the result as a single byte. Universal validation can be completed with one eth_call, without a pre-deployed contract. The ethers example appends the signer, hash, and signature, encoded as ['address', 'bytes32', 'bytes'], to the ValidateSigOffchain bytecode, sends that data with provider.call, and treats the signature as valid if the return value is '0x01'. You can also use a library such as Ambire's signature-validator.
Compatibility and use for ordinary contract signatures
This standard is compatible with existing signature validation work such as ERC-1271. It makes it easy to validate every signature type, including EOA signatures and EIP-712 typed data. The wrapped format can be used in place of plain ERC-1271 for all contract signatures, not only for contracts that are not yet deployed. In that case, thanks to magicBytes, a signature can be recognized as a contract signature immediately without querying the blockchain, and, as with ecrecover, the address can be recovered from the signature using only create2Factory and factoryCalldata.
Things to watch out for
- The same caveats as ERC-1271: The security considerations of ERC-1271 apply as they are.
- Reentrancy risk from deployment: Deploying a contract requires CALL rather than STATICCALL, which creates a reentrancy problem. The reference implementation mitigates this by having the validation function always revert when there are side effects and extracting the actual result from the revert data. For cases where reentrancy is not a concern, isValidSigWithSideEffects is provided separately. On-chain validation usually assumes the wallet is already deployed, so this standard is likely to be used more often for off-chain validation.
- Permission setup at deployment: Whether a contract is set up with the correct permissions at deployment so that meaningful signature validation is possible is outside the scope of this standard and depends on the implementation. However, because of how CREATE2 works, changing the bytecode or the constructor call data inside the signature changes the deployment address and causes validation to fail, so privilege escalation through this route is not possible.
- Contract accounts that change their authorization method: A contract account can change its authorization method dynamically. This standard mitigates the problem by design: even when validating a pre-deployment signature, if the contract is already deployed, it calls the currently deployed version to perform the check.
- Replay protection and other networks: In most cases, replay protection must be implemented. This standard adds one more dimension to it. Even a signature that has been invalidated by an authorized key change may pass validation on another network if it was valid at deployment time and the contract can be deployed there with the same factory address and bytecode.