Abstract
Contracts can sign verifyable messages via ERC-1271.
However, if the contract is not deployed yet, ERC-1271 verification is impossible, as you can't call the isValidSignature
function on said contract.
We propose a standard way for any contract or off-chain actor to verify whether a signature on behalf of a given counterfactual contract (that is not deployed yet) is valid. This standarad way extends ERC-1271.
Motivation
With the popularity of ERC-4337 and other account abstraction initiatives, we often find that the best user experience for contract wallets is to defer contract deployment until the first user transaction, therefore not burdening the user with an additional deploy step before they can use their account. However, at the same time, many dApps expect signatures, not only for interactions, but also just for logging in.
As such, contract wallets have been limited in their ability to sign messages before their de-facto deployment, which is often done on the first transaction.
Furthermore, not being able to sign messages from counterfactual contracts has always been a limitation of ERC-1271.
The initial motivation is discussed in the eth-infinitism/account-abstraction repo.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
Quoting ERC-1271,
isValidSignature
can call arbitrary methods to validate a given signature, which could be context dependent (e.g. time based or state based), EOA dependent (e.g. signers authorization level within smart wallet), signature scheme Dependent (e.g. ECDSA, multisig, BLS), etc.This function should be implemented by contracts which desire to sign messages (e.g. smart contract wallets, DAOs, multisignature wallets, etc.) Applications wanting to support contract signatures should call this method if the signer is a contract.
We use the same isValidSignature
function, but we add a new wrapper signature format, that signing contracts MAY use before they're deployed, in order to allow support for verification.
The signature verifier MUST perform a contract deployment before attempting to call isValidSignature
if the wrapper signature format is detected.
The wrapper format is detected by checking if the signature ends in magicBytes
, which MUST be defined as 0x6492649264926492649264926492649264926492649264926492649264926492
.
It is RECOMMENDED to use this ERC with CREATE2 contracts, as their deploy address is always predictable.
Signer side
The signer will normally be a contract wallet, but it could be any other counterfactual contract as well.
- If the contract is deployed, produce a normal ERC-1271 signature
- If the contract is not deployed yet, wrap the signature as follows:
concat(abi.encodePacked((create2Factory, factoryCalldata, originalERC1271Signature), (address, bytes32, bytes, bytes, bytes)), magicBytes)
Note that we're passing factoryCalldata
instead of salt
and bytecode
. We do this in order to make verification compliant with any factory interface. We do not need to calculate the address based on create2Factory
/salt
/bytecode
, because ERC-1271 verification presumes we already know the account address we're verifying the signature for.
Verifier side
Full signature verification MUST be performed in the following order:
- check if the signature ends with magic bytes, in which case do an
eth_call
to a multicall contract that will call the factory first with thefactoryCalldata
and deploy the contract; Then, callcontract.isValidSignature
as usual - check if there's contract code at the address. If so perform ERC-1271 verification as usual by invoking
isValidSignature
- If all this fails, try
ecrecover
verification
Rationale
We believe that wrapping the signature in a way that allows to pass the deploy data is the only clean way to implement this, as it's completely contract agnostic, but also easy to verify.
The wrapper format ends in magicBytes
, which ends with a 0x92
, which makes it is impossible for it to collide with a valid ecrecover
signature if packed in the r,s,v
format, as 0x92
is not a valid value for v
. To avoid collisions with normal ERC-1271, magicBytes
itself is also quite long (bytes32
).
The verification order to ensure correct verification is based on the following rules:
- checking for
magicBytes
MUST happen before the usual ERC-1271 check in order to allow counterfactual signatures to be valid even after contract deployment - checking for
magicBytes
MUST happen beforeecrecover
in order to avoid trying to verify a counterfactual contract signature viaecrecover
if such is clearly identifiable - checking
ecrecover
MUST NOT happen before ERC-1271 verification, because a contract may use a signature format that also happens to be a validecrecover
signature for an EOA with a different address. One such example is a contract that's a wallet controlled by said EOA.
Backwards Compatibility
This ERC is backward compatible with previous work on signature validation, including ERC-1271 and allows for easy verification of all signature types, including EOA signatures and typed data (EIP-712).
Reference Implementation
Example implementation of a verification contract:
contract ERC6492Verifier {
/**
* @notice Verifies that the signature is valid for that signer and hash
*/
function isValidUniversalSignature(
address _signer,
bytes32 _hash,
bytes calldata _signature
) external override returns (bool) {
uint32 contractSize;
assembly {
size := extcodesize(_signer)
}
if (bytes32(_signature[_signature.length-32:signature.length]) == 0x6492649264926492649264926492649264926492649264926492649264926492) {
_signature.trimToSize(_signature.length - 4);
address create2Factory;
bytes memory factoryCalldata;
(create2Factory, factoryCalldata, sig) = abi.decode(sig, (address, bytes, bytes));
if (contractSize == 0) {
(bool success, ) = create2Factory.call(factoryCalldata);
if (!success) return false;
}
return IERC1271Wallet(_signer).isValidSignature(_hash, sig) == 0x1626ba7e;
}
if (contractSize > 0) {
return IERC1271Wallet(_signer).isValidSignature(_hash, _signature) == 0x1626ba7e;
}
// ecrecover verification
require(_signature.length == 65, "SignatureValidator#recoverSigner: invalid signature length");
uint8 v = uint8(_signature[64]);
bytes32 r = _signature.readBytes32(0);
bytes32 s = _signature.readBytes32(32);
if (v != 27 && v != 28) {
revert("SignatureValidator#recoverSigner: invalid signature 'v' value");
}
return ecrecover(_hash, v, r, s) == _signer;
}
}
This verification contract SHOULD be deployed as a singleton on Ethereum mainnet or any other EVM chain and it SHOULD be invoked off-chain via eth_call
or equivalent RPC methods by the verifier. Note that even though this function does not have a view
modifier, it can still be invoked off-chain via an eth_call
.
Alternatively, the verifier may perform the same logic using a singleton contract like MakerDAO's multicall, by performing the aforementioned checks off-chain.
Security Considerations
The same considerations as ERC-1271 apply.
However, deploying a contract requires a CALL
rather than a STATICCALL
, which introduces reentrancy concerns. As such, we recommend that this ERC is mainly used for off-chain signature validation. It is possible to use it for on-chain signature validation as well, but such a case is by definition redundant, as we are presuming that the contract will be deployed in that case.
Another out-of-scope security consideration worth mentioning is whether the contract is going to be set-up with the correct permissions at deploy time, in order to allow for meaningful signature verification. By design, this is up to the implementation, but it's worth noting that thanks to how CREATE2 works, changing the bytecode or contructor callcode in the signature will not allow you to escalate permissions as it will change the deploy address and therefore make verification fail.
Copyright
Copyright and related rights waived via CC0.