diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7dde004..5b5dd0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,27 @@ jobs: - name: Run tests run: forge test + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.21.5 + cache: False + + - name: Install `jd` CLI + run: go install github.com/josephburnett/jd@latest + + - name: Ensure correctness of the `IERC5564Announcer` interface + run: | + diff=$(jd -set <(jq '.abi' out/ERC5564Announcer.sol/ERC5564Announcer.json) <(jq '.abi' out/IERC5564Announcer.sol/IERC5564Announcer.json)) + if [[ -n $diff ]]; then exit 1; fi + + - name: Ensure correctness of the `IERC6538Registry` interface + run: | + echo $(jd -set <(jq '.abi' out/ERC6538Registry.sol/ERC6538Registry.json) <(jq '.abi' out/IERC6538Registry.sol/IERC6538Registry.json)) > diff.txt + echo "@ [[\"set\"],{}] - {\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"}" > expected_diff.txt + diff=$(diff diff.txt expected_diff.txt) + if [[ -n $diff ]]; then exit 1; fi + coverage: runs-on: ubuntu-latest steps: @@ -110,7 +131,7 @@ jobs: - uses: actions/checkout@v3 - name: Run Slither - uses: crytic/slither-action@v0.3.0 + uses: crytic/slither-action@v0.3.1 id: slither # Required to reference this step in the next step. with: fail-on: none # Required to avoid failing the CI run regardless of findings. diff --git a/foundry.toml b/foundry.toml index 60a9a9f..996f0b9 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ evm_version = "paris" optimizer = true optimizer_runs = 10_000_000 - solc_version = "0.8.20" + solc_version = "0.8.23" verbosity = 3 [profile.ci] diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index cc0c286..bdab950 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -1,7 +1,7 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: MIT // slither-disable-start reentrancy-benign -pragma solidity 0.8.20; +pragma solidity 0.8.23; import {Script} from "forge-std/Script.sol"; import {ERC5564Announcer} from "src/ERC5564Announcer.sol"; @@ -10,12 +10,24 @@ import {ERC6538Registry} from "src/ERC6538Registry.sol"; contract Deploy is Script { ERC5564Announcer announcer; ERC6538Registry registry; + address deployer = 0x4e59b44847b379578588920cA78FbF26c0B4956C; + bytes32 salt = ""; function run() public { + bytes memory ERC5564CreationCode = abi.encodePacked(type(ERC5564Announcer).creationCode); + bytes memory ERC6538CreationCode = abi.encodePacked(type(ERC6538Registry).creationCode); + address ERC5564ComputedAddress = + computeCreate2Address(salt, keccak256(ERC5564CreationCode), deployer); + address ERC6538ComputedAddress = + computeCreate2Address(salt, keccak256(ERC6538CreationCode), deployer); + vm.broadcast(); - announcer = new ERC5564Announcer(); + announcer = new ERC5564Announcer{salt: salt}(); vm.broadcast(); - registry = new ERC6538Registry(); + registry = new ERC6538Registry{salt: salt}(); + + require(address(announcer) == ERC5564ComputedAddress, "announce address mismatch"); + require(address(registry) == ERC6538ComputedAddress, "registry address mismatch"); } } diff --git a/src/ERC5564Announcer.sol b/src/ERC5564Announcer.sol index 5dc4965..c0f0aa1 100644 --- a/src/ERC5564Announcer.sol +++ b/src/ERC5564Announcer.sol @@ -1,13 +1,37 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; -import {IERC5564Announcer} from "./interfaces/IERC5564Announcer.sol"; - -/// @dev `ERC5564Announcer` contract to emit an `Announcement` event to broadcast information about -/// a transaction involving a stealth address. See +/// @notice `ERC5564Announcer` contract to emit an `Announcement` event to broadcast information +/// about a transaction involving a stealth address. See /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) to learn more. -contract ERC5564Announcer is IERC5564Announcer { - /// @inheritdoc IERC5564Announcer +contract ERC5564Announcer { + /// @notice Emitted when something is sent to a stealth address. + /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for + /// secp256k1, as specified in ERC-5564. + /// @param stealthAddress The computed stealth address for the recipient. + /// @param caller The caller of the `announce` function that emitted this event. + /// @param ephemeralPubKey Ephemeral public key used by the sender to derive the `stealthAddress`. + /// @param metadata Arbitrary data to emit with the event. The first byte MUST be the view tag. + /// @dev The remaining metadata can be used by the senders however they like. See + /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) for recommendations on how to structure + /// this metadata. + event Announcement( + uint256 indexed schemeId, + address indexed stealthAddress, + address indexed caller, + bytes ephemeralPubKey, + bytes metadata + ); + + /// @notice Called by integrators to emit an `Announcement` event. + /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for + /// secp256k1, as specified in ERC-5564. + /// @param stealthAddress The computed stealth address for the recipient. + /// @param ephemeralPubKey Ephemeral public key used by the sender. + /// @param metadata Arbitrary data to emit with the event. The first byte MUST be the view tag. + /// @dev The remaining metadata can be used by the senders however they like. See + /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) for recommendations on how to structure + /// this metadata. function announce( uint256 schemeId, address stealthAddress, diff --git a/src/ERC6538Registry.sol b/src/ERC6538Registry.sol index 49d422e..eca4825 100644 --- a/src/ERC6538Registry.sol +++ b/src/ERC6538Registry.sol @@ -1,45 +1,155 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; -import {IERC6538Registry} from "./interfaces/IERC6538Registry.sol"; - -/// @dev `ERC6538Registry` contract to map accounts to their stealth meta-address. See +/// @notice `ERC6538Registry` contract to map accounts to their stealth meta-address. See /// [ERC-6538](https://eips.ethereum.org/EIPS/eip-6538) to learn more. -contract ERC6538Registry is IERC6538Registry { - /// @notice Maps a registrant's identifier to the scheme ID to the stealth meta-address. +contract ERC6538Registry { + /// @notice Emitted when an invalid signature is provided to `registerKeysOnBehalf`. + error ERC6538Registry__InvalidSignature(); + + /// @notice Next nonce expected from `user` to use when signing for `registerKeysOnBehalf`. /// @dev `registrant` may be a standard 160-bit address or any other identifier. /// @dev `schemeId` is an integer identifier for the stealth address scheme. - mapping(bytes registrant => mapping(uint256 schemeId => bytes)) public stealthMetaAddressOf; + mapping(address registrant => mapping(uint256 schemeId => bytes)) public stealthMetaAddressOf; - /// @inheritdoc IERC6538Registry - function registerKeys(uint256 schemeId, bytes memory stealthMetaAddress) external { - bytes memory registrant = _toBytes(msg.sender); - stealthMetaAddressOf[registrant][schemeId] = stealthMetaAddress; - emit StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress); + /// @notice A nonce used to ensure a signature can only be used once. + /// @dev `registrant` is the user address. + /// @dev `nonce` will be incremented after each valid `registerKeysOnBehalf` call. + mapping(address registrant => uint256) public nonceOf; + + /// @notice The EIP-712 type hash used in `registerKeysOnBehalf`. + bytes32 public constant ERC6538REGISTRY_ENTRY_TYPE_HASH = + keccak256("Erc6538RegistryEntry(uint256 schemeId,bytes stealthMetaAddress,uint256 nonce)"); + + /// @notice The chain ID where this contract is initially deployed. + uint256 internal immutable INITIAL_CHAIN_ID; + + /// @notice The domain separator used in this contract. + bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; + + /// @notice Emitted when a registrant updates their stealth meta-address. + /// @param registrant The account that registered the stealth meta-address. + /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for + /// secp256k1, as specified in ERC-5564. + /// @param stealthMetaAddress The stealth meta-address. + /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) bases the format for stealth + /// meta-addresses on [ERC-3770](https://eips.ethereum.org/EIPS/eip-3770) and specifies them as: + /// st::0x: + /// The chain (`shortName`) is implicit based on the chain the `ERC6538Registry` is deployed on, + /// therefore this `stealthMetaAddress` is just the compressed `spendingPubKey` and + /// `viewingPubKey` concatenated. + event StealthMetaAddressSet( + address indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress + ); + + constructor() { + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator(); + } + + /// @notice Sets the caller's stealth meta-address for the given scheme ID. + /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for + /// secp256k1, as specified in ERC-5564. + /// @param stealthMetaAddress The stealth meta-address to register. + function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external { + stealthMetaAddressOf[msg.sender][schemeId] = stealthMetaAddress; + emit StealthMetaAddressSet(msg.sender, schemeId, stealthMetaAddress); } - /// @inheritdoc IERC6538Registry + /// @notice Sets the `registrant`'s stealth meta-address for the given scheme ID. + /// @param registrant Address of the registrant. + /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for + /// secp256k1, as specified in ERC-5564. + /// @param signature A signature from the `registrant` authorizing the registration. + /// @param stealthMetaAddress The stealth meta-address to register. + /// @dev Supports both EOA signatures and EIP-1271 signatures. + /// @dev Reverts if the signature is invalid. function registerKeysOnBehalf( address registrant, uint256 schemeId, bytes memory signature, - bytes memory stealthMetaAddress - ) external pure { - registerKeysOnBehalf(_toBytes(registrant), schemeId, signature, stealthMetaAddress); + bytes calldata stealthMetaAddress + ) external { + bytes32 dataHash; + address recoveredAddress; + + unchecked { + dataHash = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + ERC6538REGISTRY_ENTRY_TYPE_HASH, schemeId, stealthMetaAddress, nonceOf[registrant]++ + ) + ) + ) + ); + } + + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + assembly ("memory-safe") { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + recoveredAddress = ecrecover(dataHash, v, r, s); + } + + if ( + ( + (recoveredAddress == address(0) || recoveredAddress != registrant) + && ( + IERC1271(registrant).isValidSignature(dataHash, signature) + != IERC1271.isValidSignature.selector + ) + ) + ) revert ERC6538Registry__InvalidSignature(); + + stealthMetaAddressOf[registrant][schemeId] = stealthMetaAddress; + emit StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress); } - /// @inheritdoc IERC6538Registry - function registerKeysOnBehalf( - bytes memory, // registrant - uint256, // schemeId - bytes memory, // signature - bytes memory // stealthMetaAddress - ) public pure { - revert("not implemented"); + /// @notice Increments the nonce of the sender to invalidate existing signatures. + function incrementNonce() external { + unchecked { + nonceOf[msg.sender]++; + } + } + + /// @notice Returns the domain separator used in this contract. + /// @dev The domain separator is re-computed if there's a chain fork. + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator(); } - /// @dev Converts an `address` to `bytes`. - function _toBytes(address who) internal pure returns (bytes memory) { - return bytes.concat(bytes32(uint256(uint160(who)))); + /// @notice Computes the domain separator for this contract. + function _computeDomainSeparator() internal view returns (bytes32) { + return keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256("ERC6538Registry"), + keccak256("1.0"), + block.chainid, + address(this) + ) + ); } } + +/// @notice Interface of the ERC1271 standard signature validation method for contracts as defined +/// in https://eips.ethereum.org/EIPS/eip-1271[ERC-1271]. +interface IERC1271 { + /// @notice Should return whether the signature provided is valid for the provided data + /// @param hash Hash of the data to be signed + /// @param signature Signature byte array associated with _data + function isValidSignature(bytes32 hash, bytes memory signature) + external + view + returns (bytes4 magicValue); +} diff --git a/src/interfaces/IERC5564Announcer.sol b/src/interfaces/IERC5564Announcer.sol index 3550878..a47c132 100644 --- a/src/interfaces/IERC5564Announcer.sol +++ b/src/interfaces/IERC5564Announcer.sol @@ -1,18 +1,18 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; -/// @dev Interface for calling the `ERC5564Announcer` contract, which emits an `Announcement` event -/// to broadcast information about a transaction involving a stealth address. See +/// @notice Interface for calling the `ERC5564Announcer` contract, which emits an `Announcement` +/// event to broadcast information about a transaction involving a stealth address. See /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) to learn more. interface IERC5564Announcer { - /// @dev Emitted when something is sent to a stealth address. + /// @notice Emitted when something is sent to a stealth address. /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for /// secp256k1, as specified in ERC-5564. /// @param stealthAddress The computed stealth address for the recipient. /// @param caller The caller of the `announce` function that emitted this event. /// @param ephemeralPubKey Ephemeral public key used by the sender to derive the `stealthAddress`. /// @param metadata Arbitrary data to emit with the event. The first byte MUST be the view tag. - /// The remaining metadata can be used by the senders however they like. See + /// @dev The remaining metadata can be used by the senders however they like. See /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) for recommendations on how to structure /// this metadata. event Announcement( @@ -23,13 +23,13 @@ interface IERC5564Announcer { bytes metadata ); - /// @dev Called by integrators to emit an `Announcement` event. + /// @notice Called by integrators to emit an `Announcement` event. /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for /// secp256k1, as specified in ERC-5564. /// @param stealthAddress The computed stealth address for the recipient. /// @param ephemeralPubKey Ephemeral public key used by the sender. /// @param metadata Arbitrary data to emit with the event. The first byte MUST be the view tag. - /// The remaining metadata can be used by the senders however they like. See + /// @dev The remaining metadata can be used by the senders however they like. See /// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) for recommendations on how to structure /// this metadata. function announce( diff --git a/src/interfaces/IERC6538Registry.sol b/src/interfaces/IERC6538Registry.sol index bb70131..4e49efc 100644 --- a/src/interfaces/IERC6538Registry.sol +++ b/src/interfaces/IERC6538Registry.sol @@ -1,9 +1,12 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; /// @dev Interface for calling the `ERC6538Registry` contract to map accounts to their stealth /// meta-address. See [ERC-6538](https://eips.ethereum.org/EIPS/eip-6538) to learn more. interface IERC6538Registry { + /// @notice Emitted when an invalid signature is provided to `registerKeysOnBehalf`. + error ERC6538Registry__InvalidSignature(); + /// @dev Emitted when a registrant updates their stealth meta-address. /// @param registrant The account that registered the stealth meta-address. /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 0 for @@ -16,14 +19,14 @@ interface IERC6538Registry { /// therefore this `stealthMetaAddress` is just the `spendingPubKey` and `viewingPubKey` /// concatenated. event StealthMetaAddressSet( - bytes indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress + address indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress ); /// @notice Sets the caller's stealth meta-address for the given scheme ID. /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 0 for /// secp256k1, as specified in ERC-5564. /// @param stealthMetaAddress The stealth meta-address to register. - function registerKeys(uint256 schemeId, bytes memory stealthMetaAddress) external; + function registerKeys(uint256 schemeId, bytes calldata stealthMetaAddress) external; /// @notice Sets the `registrant`'s stealth meta-address for the given scheme ID. /// @param registrant Address of the registrant. @@ -37,21 +40,24 @@ interface IERC6538Registry { address registrant, uint256 schemeId, bytes memory signature, - bytes memory stealthMetaAddress + bytes calldata stealthMetaAddress ) external; - /// @notice Sets the `registrant`s stealth meta-address for the given scheme ID. - /// @param registrant Recipient identifier, such as an address. - /// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 0 for - /// secp256k1, as specified in ERC-5564. - /// @param signature A signature from the `registrant` authorizing the registration. - /// @param stealthMetaAddress The stealth meta-address to register. - /// @dev Supports both EOA signatures and EIP-1271 signatures. - /// @dev Reverts if the signature is invalid. - function registerKeysOnBehalf( - bytes memory registrant, - uint256 schemeId, - bytes memory signature, - bytes memory stealthMetaAddress - ) external; + /// @notice Increments the nonce of the sender to invalidate existing signatures. + function incrementNonce() external; + + /// @notice Returns the domain separator used in this contract. + function DOMAIN_SEPARATOR() external view returns (bytes32); + + /// @notice Returns the stealth meta-address for the given `registrant` and `schemeId`. + function stealthMetaAddressOf(address registrant, uint256 schemeId) + external + view + returns (bytes memory); + + /// @notice Returns the EIP-712 type hash used in `registerKeysOnBehalf`. + function ERC6538REGISTRY_ENTRY_TYPE_HASH() external view returns (bytes32); + + /// @notice Returns the nonce of the given `registrant`. + function nonceOf(address registrant) external view returns (uint256); } diff --git a/test/Deploy.t.sol b/test/Deploy.t.sol new file mode 100644 index 0000000..8e7f39b --- /dev/null +++ b/test/Deploy.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import {Deploy} from "script/Deploy.s.sol"; +import {Test} from "forge-std/Test.sol"; +import {ERC5564Announcer} from "src/ERC5564Announcer.sol"; +import {ERC6538Registry} from "src/ERC6538Registry.sol"; + +contract DeployTest is Test, Deploy { + bytes announcerContractCode; + ERC5564Announcer announcerTestDeployment; + + function setUp() public { + announcerTestDeployment = new ERC5564Announcer(); + announcerContractCode = address(announcerTestDeployment).code; + } + + function test_Deploy() external { + bytes memory erc5564CreationCode = abi.encodePacked(type(ERC5564Announcer).creationCode); + bytes memory erc6538CreationCode = abi.encodePacked(type(ERC6538Registry).creationCode); + + address erc5564ComputedAddress = + computeCreate2Address(salt, keccak256(erc5564CreationCode), deployer); + address erc6538ComputedAddress = + computeCreate2Address(salt, keccak256(erc6538CreationCode), deployer); + + require(erc5564ComputedAddress.code.length == 0); + require(erc6538ComputedAddress.code.length == 0); + + // Run deploy script + Deploy.run(); + + assertTrue(erc5564ComputedAddress.code.length > 0); + assertTrue(erc6538ComputedAddress.code.length > 0); + assertEq(erc5564ComputedAddress.code, announcerContractCode); + } +} diff --git a/test/ERC5564Announcer.t.sol b/test/ERC5564Announcer.t.sol index c76236b..d5e3afc 100644 --- a/test/ERC5564Announcer.t.sol +++ b/test/ERC5564Announcer.t.sol @@ -1,5 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; import {Test} from "forge-std/Test.sol"; import {Deploy} from "script/Deploy.s.sol"; @@ -38,7 +38,7 @@ contract Announce is ERC5564AnnouncerTest { /// forge-config: default.fuzz.runs = 1 /// forge-config: ci.fuzz.runs = 1 /// forge-config: lite.fuzz.runs = 1 - function testFuzz_NeverReverts( + function testFuzz_AlwaysSucceeds( uint256 schemeId, address stealthAddress, address caller, diff --git a/test/ERC6538Registry.t.sol b/test/ERC6538Registry.t.sol index 06ac6b6..7608175 100644 --- a/test/ERC6538Registry.t.sol +++ b/test/ERC6538Registry.t.sol @@ -1,44 +1,124 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.20; +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; import {Test} from "forge-std/Test.sol"; import {Deploy} from "script/Deploy.s.sol"; +import {ERC6538Registry, IERC1271} from "src/ERC6538Registry.sol"; contract ERC6538RegistryTest is Test, Deploy { + error ERC6538Registry__InvalidSignature(); + event StealthMetaAddressSet( - bytes indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress + address indexed registrant, uint256 indexed schemeId, bytes stealthMetaAddress ); function setUp() public { Deploy.run(); } - function toBytes(address who) internal pure returns (bytes memory) { - return bytes.concat(bytes32(uint256(uint160(who)))); + // Test helper to compute EIP712 domain separator for a given chain id and deployment address + function _computeDomainSeparator(uint256 _chainId, address _registryAddress) + public + pure + returns (bytes32) + { + return keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256("ERC6538Registry"), + keccak256("1.0"), + _chainId, + _registryAddress + ) + ); + } + + function _generateRegistrationSignature( + uint256 _registrantPrivateKey, + uint256 _schemeId, + bytes memory _stealthMetaAddress, + uint256 _nonce + ) public view returns (bytes memory _signature) { + bytes32 _dataHash = keccak256( + abi.encode(registry.ERC6538REGISTRY_ENTRY_TYPE_HASH(), _schemeId, _stealthMetaAddress, _nonce) + ); + bytes32 _hash = keccak256(abi.encodePacked("\x19\x01", registry.DOMAIN_SEPARATOR(), _dataHash)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_registrantPrivateKey, _hash); + _signature = abi.encodePacked(r, s, v); + } + + function _notVmOrConsole(address _address) public pure { + // This function is used to avoid calling the VM or the console, which would return error + // messages. It is used in tests where we want assert a function reverts without a message. + vm.assume(_address != address(vm)); + vm.assume(_address != address(address(0x000000000000000000636F6e736F6c652e6c6f67))); + } +} + +// Test harness to expose internal contract methods for test purpose only +contract ERC6538RegistryHarness is ERC6538Registry { + function exposed_INITIAL_CHAIN_ID() public view returns (uint256) { + return INITIAL_CHAIN_ID; + } + + function exposed_INITIAL_DOMAIN_SEPARATOR() public view returns (bytes32) { + return INITIAL_DOMAIN_SEPARATOR; + } +} + +contract Constructor is ERC6538RegistryTest { + function test_SetsTheInitialChainId() external { + ERC6538RegistryHarness _registry = new ERC6538RegistryHarness(); + assertEq(_registry.exposed_INITIAL_CHAIN_ID(), block.chainid); + } + + function test_SetsTheInitialDomainSeparator() external { + ERC6538RegistryHarness _registry = new ERC6538RegistryHarness(); + assertEq( + _registry.exposed_INITIAL_DOMAIN_SEPARATOR(), + _computeDomainSeparator(block.chainid, address(_registry)) + ); } } contract RegisterKeys is ERC6538RegistryTest { - function testFuzz_EmitsStealthMetaAddressSetEvent( + function testFuzz_EmitsASetEvent( address caller, uint256 schemeId, bytes memory stealthMetaAddress ) external { vm.prank(caller); vm.expectEmit(); - emit StealthMetaAddressSet(toBytes(caller), schemeId, stealthMetaAddress); + emit StealthMetaAddressSet(caller, schemeId, stealthMetaAddress); registry.registerKeys(schemeId, stealthMetaAddress); } - function testFuzz_CorrectlyMapsRegistrantToSchemeIdToStealthMetaAddressInStorage( + function testFuzz_SetsTheStealthMetaAddressForANewRegistrant( address caller, uint256 schemeId, bytes memory stealthMetaAddress ) external { - assertEq(registry.stealthMetaAddressOf(toBytes(caller), schemeId), ""); vm.prank(caller); registry.registerKeys(schemeId, stealthMetaAddress); - assertEq(registry.stealthMetaAddressOf(toBytes(caller), schemeId), stealthMetaAddress); + + assertEq(registry.stealthMetaAddressOf((caller), schemeId), stealthMetaAddress); + } + + function testFuzz_UpdatesTheStealthMetaAddressForAnExistingRegistrant( + address caller, + uint256 schemeId, + bytes memory stealthMetaAddress1, + bytes memory stealthMetaAddress2 + ) external { + vm.prank(caller); + registry.registerKeys(schemeId, stealthMetaAddress1); + assertEq(registry.stealthMetaAddressOf((caller), schemeId), stealthMetaAddress1); + + vm.prank(caller); + registry.registerKeys(schemeId, stealthMetaAddress2); + assertEq(registry.stealthMetaAddressOf((caller), schemeId), stealthMetaAddress2); } // This test is a subset of `testFuzz_EmitsStealthMetaAddressSetEvent`, and is mainly present to @@ -47,24 +127,260 @@ contract RegisterKeys is ERC6538RegistryTest { /// forge-config: default.fuzz.runs = 1 /// forge-config: ci.fuzz.runs = 1 /// forge-config: lite.fuzz.runs = 1 - function testFuzz_NeverReverts(address caller, uint256 schemeId, bytes memory stealthMetaAddress) - external - { + function testFuzz_AlwaysSucceeds( + address caller, + uint256 schemeId, + bytes memory stealthMetaAddress + ) external { vm.prank(caller); registry.registerKeys(schemeId, stealthMetaAddress); } } -contract RegisterKeysOnBehalf_Address is ERC6538RegistryTest { - function test_NotImplemented() external { - vm.expectRevert("not implemented"); - registry.registerKeysOnBehalf(address(0), 0, "", ""); +contract RegisterKeysOnBehalf is ERC6538RegistryTest { + function testFuzz_SetsTheStealthMetaAddressForANewRegistrantWhenProvidedAValidErc712Signature( + string memory registrantSeed, + uint256 schemeId, + bytes memory stealthMetaAddress + ) external { + (address registrant, uint256 registrantPrivateKey) = makeAddrAndKey(registrantSeed); + bytes memory signature = + _generateRegistrationSignature(registrantPrivateKey, schemeId, stealthMetaAddress, 0); + + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + assertEq(registry.stealthMetaAddressOf(registrant, schemeId), stealthMetaAddress); + } + + function testFuzz_EmitsAStealthMetaAddressSetEventForANewRegistrantWhenProvidedAValidErc712Signature( + string memory registrantSeed, + uint256 schemeId, + bytes memory stealthMetaAddress + ) external { + (address registrant, uint256 registrantPrivateKey) = makeAddrAndKey(registrantSeed); + bytes memory signature = + _generateRegistrationSignature(registrantPrivateKey, schemeId, stealthMetaAddress, 0); + + vm.expectEmit(); + emit StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress); + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + } + + function testFuzz_SetsTheStealthMetaAddressForANewRegistrantWhenProvidedAValidErc1271Signature( + uint256 schemeId, + bytes memory stealthMetaAddress, + bytes memory signature + ) external { + MockERC1271Signer mockRegistrant = new MockERC1271Signer(); + mockRegistrant.setResponse__isValidSignature(true); + + registry.registerKeysOnBehalf(address(mockRegistrant), schemeId, signature, stealthMetaAddress); + assertEq(registry.stealthMetaAddressOf(address(mockRegistrant), schemeId), stealthMetaAddress); + } + + function testFuzz_EmitsAStealthMetaAddressSetEventForANewRegistrantWhenProvidedAValidErc1271Signature( + uint256 schemeId, + bytes memory stealthMetaAddress, + bytes memory signature + ) external { + MockERC1271Signer mockRegistrant = new MockERC1271Signer(); + mockRegistrant.setResponse__isValidSignature(true); + + vm.expectEmit(); + emit StealthMetaAddressSet(address(mockRegistrant), schemeId, stealthMetaAddress); + registry.registerKeysOnBehalf(address(mockRegistrant), schemeId, signature, stealthMetaAddress); + } + + function testFuzz_UpdatesTheStealthMetaAddressForAnExistingRegistrantWhenProvidedAValidErc712Signature( + string memory registrantSeed, + uint256 schemeId, + bytes memory stealthMetaAddress, + uint256 numOfUpdates + ) external { + numOfUpdates = bound(numOfUpdates, 2, 50); + (address registrant, uint256 registrantPrivateKey) = makeAddrAndKey(registrantSeed); + + for (uint256 nonce = 0; nonce < numOfUpdates; nonce++) { + bytes memory signature = + _generateRegistrationSignature(registrantPrivateKey, schemeId, stealthMetaAddress, nonce); + + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + assertEq(registry.stealthMetaAddressOf(registrant, schemeId), stealthMetaAddress); + } + } + + function testFuzz_UpdatesTheStealthMetaAddressForAnExistingRegistrantWhenProvidedAValidErc1271Signature( + uint256 schemeId, + bytes memory stealthMetaAddress, + bytes memory signature, + uint256 numOfUpdates + ) external { + numOfUpdates = bound(numOfUpdates, 2, 50); + + MockERC1271Signer mockRegistrant = new MockERC1271Signer(); + mockRegistrant.setResponse__isValidSignature(true); + + for (uint256 nonce = 0; nonce < numOfUpdates; nonce++) { + registry.registerKeysOnBehalf( + address(mockRegistrant), schemeId, signature, stealthMetaAddress + ); + assertEq(registry.stealthMetaAddressOf(address(mockRegistrant), schemeId), stealthMetaAddress); + } + } + + function testFuzz_RevertIf_TheDataIsErc712SignedByAnAddressOtherThanTheRegistrant( + string memory seed, + address registrant, + uint256 schemeId, + bytes memory stealthMetaAddress + ) external { + (address notRegistrant, uint256 notRegistrantPrivateKey) = makeAddrAndKey(seed); + vm.assume(notRegistrant != registrant); + _notVmOrConsole(registrant); + bytes memory signature = + _generateRegistrationSignature(notRegistrantPrivateKey, schemeId, stealthMetaAddress, 0); + + vm.expectRevert(bytes("")); + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + } + + function testFuzz_RevertIf_TheRegistrantErc712SignsOverTheWrongSchemeId( + string memory registrantSeed, + uint256 schemeId, + uint256 wrongSchemeId, + bytes memory stealthMetaAddress + ) external { + vm.assume(schemeId != wrongSchemeId); + (address registrant, uint256 registrantPrivateKey) = makeAddrAndKey(registrantSeed); + + bytes memory signature = + _generateRegistrationSignature(registrantPrivateKey, wrongSchemeId, stealthMetaAddress, 0); + + vm.expectRevert(bytes("")); + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + } + + function testFuzz_RevertIf_TheRegistrantErc712SignsOverTheWrongStealthMetaAddress( + string memory registrantSeed, + uint256 schemeId, + bytes memory stealthMetaAddress, + bytes memory wrongStealthAMetaAddress + ) external { + vm.assume(keccak256(stealthMetaAddress) != keccak256(wrongStealthAMetaAddress)); + (address registrant, uint256 registrantPrivateKey) = makeAddrAndKey(registrantSeed); + + bytes memory signature = + _generateRegistrationSignature(registrantPrivateKey, schemeId, wrongStealthAMetaAddress, 0); + + vm.expectRevert(bytes("")); + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + } + + function testFuzz_RevertIf_ANewRegistrantErc712SignsOverANonZeroNonce( + string memory registrantSeed, + uint256 schemeId, + bytes memory stealthMetaAddress, + uint256 nonce + ) external { + vm.assume(nonce != 0); + (address registrant, uint256 registrantPrivateKey) = makeAddrAndKey(registrantSeed); + bytes memory signature = + _generateRegistrationSignature(registrantPrivateKey, schemeId, stealthMetaAddress, nonce); + + vm.expectRevert(bytes("")); + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + } + + function testFuzz_RevertIf_APreviouslyUsedErc712SignatureIsReplayed( + string memory registrantSeed, + uint256 schemeId, + bytes memory stealthMetaAddress + ) external { + (address registrant, uint256 registrantPrivateKey) = makeAddrAndKey(registrantSeed); + bytes memory signature = + _generateRegistrationSignature(registrantPrivateKey, schemeId, stealthMetaAddress, 0); + + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + + vm.expectRevert(bytes("")); + registry.registerKeysOnBehalf(registrant, schemeId, signature, stealthMetaAddress); + } + + function testFuzz_RevertIf_TheErc1271SignatureIsNotValid( + uint256 schemeId, + bytes memory stealthMetaAddress, + bytes memory signature + ) external { + MockERC1271Signer mockRegistrant = new MockERC1271Signer(); + mockRegistrant.setResponse__isValidSignature(false); + + vm.expectRevert(ERC6538Registry__InvalidSignature.selector); + registry.registerKeysOnBehalf(address(mockRegistrant), schemeId, signature, stealthMetaAddress); + } + + function testFuzz_RevertIf_AnEmptySignatureIsSubmitted( + address registrant, + uint256 schemeId, + bytes memory stealthMetaAddress + ) external { + _notVmOrConsole(registrant); + vm.expectRevert(bytes("")); + registry.registerKeysOnBehalf(registrant, schemeId, "", stealthMetaAddress); } } -contract RegisterKeysOnBehalf_Bytes is ERC6538RegistryTest { - function test_NotImplemented() external { - vm.expectRevert("not implemented"); - registry.registerKeysOnBehalf(bytes("0"), 0, "", ""); +contract IncrementNonce is ERC6538RegistryTest { + function testFuzz_IncrementsTheNonceOfTheCaller(address registrant, uint256 numOfCalls) external { + numOfCalls = bound(numOfCalls, 2, 50); + + for (uint256 i = 1; i < numOfCalls; i++) { + vm.prank(registrant); + registry.incrementNonce(); + + assertEq(registry.nonceOf(registrant), i); + } + } +} + +contract Domain_Separator is ERC6538RegistryTest { + function test_ReturnsTheValueComputedAtDeploymentIfChainIdRemainsUnchanged() external { + ERC6538RegistryHarness _registry = new ERC6538RegistryHarness(); + assertEq(_registry.DOMAIN_SEPARATOR(), _registry.exposed_INITIAL_DOMAIN_SEPARATOR()); + } + + function testFuzz_ReturnsARecomputedValueIfTheChainIdChanges(uint256 chainId1, uint256 chainId2) + external + { + chainId1 = bound(chainId1, 1, 2 ** 64 - 1); + chainId2 = bound(chainId2, 1, 2 ** 64 - 1); + + vm.chainId(chainId1); + assertEq(registry.DOMAIN_SEPARATOR(), _computeDomainSeparator(chainId1, address(registry))); + + vm.chainId(chainId2); + assertEq(registry.DOMAIN_SEPARATOR(), _computeDomainSeparator(chainId2, address(registry))); + } +} + +contract MockERC1271Signer is IERC1271 { + bytes4 public constant MAGICVALUE = 0x1626ba7e; + bytes4 public response__isValidSignature; + + function setResponse__isValidSignature(bool _nextResponse) external { + if (_nextResponse) { + // If the mock should signal the signature is valid, it should return the MAGICVALUE + response__isValidSignature = MAGICVALUE; + } else { + // If the mock should signal it is not valid, we'll return an arbitrary four bytes derived + // from the address where the mock happens to be deployed + response__isValidSignature = bytes4(keccak256(abi.encode(address(this)))); + } + } + + function isValidSignature(bytes32, /* hash */ bytes memory /* signature */ ) + external + view + returns (bytes4 magicValue) + { + magicValue = response__isValidSignature; } }