Contract Provenance Verification with CREATE2 (Updated)
Cheap cryptographic verification that contract B was created by contract A
Note: This is an updated version, which incurs much less overhead in creation costs since it accesses the bytecode in a different way.
Just want the code? Github gist here.
Simple Summary
A cryptographic way to verify if a contract implementing a certain protocol was implemented by another contract.
Abstract
Sometimes it may be necessary to know if a contract was created by another contract, for example in multi stakeholder applications where contracts are minted and upgraded on behalf of users, and the contract code is trusted.
It is necessary due the possibility that someone may try to write a contract that uses the protocol in an incompatible way, which could be a security risk.
Motivation
The most obvious way to do this is to keep a mapping such as:
mapping (address => bool) mintedContracts;
However this incurs a storage cost on each contract creation and verification. Storage cost is 20,000 for initial write and 2,100 for initial read.
Additionally, if the mapping is stored in an upgradeable contract, it is easy to delete entries, or add entries that should not exist, which may create uncertainty from a security or legal perspective, or if the mapping is implemented inside an additional unmanaged contract, incurs additional gas fees on each call (so perhaps ~5000 gas on first call).
However, there is a simple cryptographic way to achieve the same thing, which incurs much lower gas fees, and is also non revocable (or at least, not more revocable than storing the mapping in an additional unmanaged contract).
Specification
Contract creation
In order to enable the provenance verification of a contract Mintee, it should receive some additional data in it’s constructor, and have a function to retrieve that data:
contract Mintee {
bytes32 immutable _salt;
bytes32 immutable _codeHash;
constructor(bytes32 salt, uint arg1, ...) {
_salt = salt;
_codeHash = _deployGetCodeHash();
}
function _deployGetCodeHash() internal pure returns (bytes32 codeHash) {
// The code hash that is used in the create2 address preimage is the hash of
// all of the creation code, which we can access using codecopy.
assembly {
let code := mload(0x40)
// don't need to update mem position, we're not using it again?
//mstore(0x40, add(code, add(0x20, mul(0x20, div(codesize(), 0x20)))))
codecopy(code, 0, codesize())
codeHash := keccak256(code, codesize())
}
}
function getProvenanceData() external returns (bytes32, bytes32) {
return (_salt, _codeHash);
}
}The overhead for the contract to implement this interface is minimal; it does not require extra storage slots (at least in Solidity, thanks to the immutable keyword), it only requires passing of the contract salt in the constructor (the previous version was passing the creation code, but it turns out this is accessible via `codecopy`.
The other part of contract creation, on the side of the Minter, is a regular create2, which additionally passes the arguments into the Mintee:
bytes32 salt = "someUniqueData";
Mintee mintee = new Mintee{salt: salt}(salt, arg1, ...);
Verification
In order to verify, we just need to know the address of the Minter and Mintee, and we can reconstruct the original address creation preimage and verify it against the address of the Mintee:
function verifyProvenance(address minter, Mintee mintee) view
{
(bytes32 salt, bytes32 codeHash) = mintee.getProvenanceData();
address addr = Create2.computeAddress(salt, bytecodeHash, minter);
require(computed == address(target), "Invalid Provenance Data");
}Why does this work?
In short, it works because it is not possible (given the security of the hash function) to create fake provenance data to get a match, since there is an input to the hash function which an attacker cannot control (the minter address).
Gas Cost
With a little optimisation, we can get the gas cost down quite low. Here is an optimised verification function:
function verifyProvenance(address creator, address target) view returns (uint salt) {
address computed;
/// @solidity memory-safe-assembly
assembly {
// Initialise hash image
let ptr := mload(0x40)
mstore(ptr, creator)
// Store the method selector into scratch space
mstore(0, hex"2f00ef5c") // selector for "getProvenanceData()"
// Get the provenance data and write it directly into the hash image
// We don't check if the call fails, the end result will be negative
pop(staticcall(1000, target, 0, 4, add(ptr, 0x20), 64))
// Copy the salt out
salt := mload(add(ptr, 0x20))
let start := add(ptr, 0x0b)
mstore8(start, 0xff)
computed := keccak256(start, 85)
}
require(computed == address(target), "Invalid Provenance Data");
}
Assuming that mintee contract will be or has already been loaded in the same transaction, This comes out to about 450 gas; a pretty good deal for something that might have involved additional storage and perhaps even an additional contract!
This function also returns the retrieved salt; this could be useful, since the salt is an opportunity to store additional metadata, along with some key that makes it unique.