Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update ERC-6538 contract and tests #5

Merged
merged 38 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
90956d3
Update contract and tests to use address for the registrant
garyghayrat Dec 14, 2023
cbb2926
Add salt to deploy script so it uses Create2
garyghayrat Dec 14, 2023
eec3747
Format using scopelint
garyghayrat Dec 14, 2023
b475760
Add passing signature verification test with oz's SignatureChecker
garyghayrat Dec 15, 2023
1031061
Take out OZ SignatureChecker dependency
garyghayrat Dec 15, 2023
e71c38b
Add nonce tracking
garyghayrat Dec 18, 2023
6cb358a
Require contract address matches precomputed address
garyghayrat Dec 18, 2023
12eedad
Support EIP-712
garyghayrat Dec 18, 2023
606e51c
Add `ERC1271MockContract` and test
garyghayrat Dec 19, 2023
835c2c4
Change contract license to MIT
garyghayrat Dec 20, 2023
4460b6e
Add natspec and incrementNonce method
garyghayrat Dec 21, 2023
7284ded
Update Interface solidity version
garyghayrat Dec 21, 2023
aa80734
Update natspec
garyghayrat Dec 21, 2023
c537910
clean up comment
garyghayrat Dec 21, 2023
12039aa
Update documentation
garyghayrat Jan 4, 2024
7997644
Add deploy script test
garyghayrat Jan 5, 2024
9372812
Update comments
garyghayrat Jan 9, 2024
b2af4c6
Simplify the contract
garyghayrat Jan 10, 2024
a5898bd
Simplify ERC712,1271 signature verification process
garyghayrat Jan 11, 2024
2384caa
Update comments and remove unused function
garyghayrat Jan 16, 2024
075b13d
Add `incrementNonce` and recompute domain seperator if chain forks
garyghayrat Jan 17, 2024
d59b26f
Rename tests
garyghayrat Jan 17, 2024
f10e5dd
Increase test coverage
garyghayrat Jan 17, 2024
7cfeddc
Style update and remove `v,r,s`
garyghayrat Jan 18, 2024
d8140ae
Fix typeHash usage
garyghayrat Jan 25, 2024
bce18d4
Add custom error
garyghayrat Jan 30, 2024
a9b7db1
Add standalone type hash definition and remove `regitrant` from it
garyghayrat Jan 30, 2024
bf34791
Change some `@dev` to `@notice`
garyghayrat Jan 30, 2024
0c32536
Remove 5564 interface inheritence
garyghayrat Jan 30, 2024
a5b3e4d
Update tests to improve `scopelint spec`
garyghayrat Jan 30, 2024
aa7965e
Update IERC6538
garyghayrat Jan 30, 2024
e403dce
Add action to check interface is in sync w/ contract
garyghayrat Jan 30, 2024
e5c6598
Set up Go in CI
garyghayrat Jan 30, 2024
07861cd
Extract abi part to compare and update `IERC6538`
garyghayrat Jan 30, 2024
26ffbf7
Fix CI for IERC6538Registry check
garyghayrat Jan 30, 2024
3a5aa7c
Update test names to be verbose for scopelint spec
garyghayrat Jan 31, 2024
7e34641
Update CI to [email protected]
garyghayrat Jan 31, 2024
df75e3e
Correct the registry domain separator and refactor test suite
apbendi Feb 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
18 changes: 15 additions & 3 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
// 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";
Expand All @@ -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");
}
}
4 changes: 2 additions & 2 deletions src/ERC5564Announcer.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

Fixed Show fixed Hide fixed
import {IERC5564Announcer} from "./interfaces/IERC5564Announcer.sol";

apbendi marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
241 changes: 221 additions & 20 deletions src/ERC6538Registry.sol
Original file line number Diff line number Diff line change
@@ -1,21 +1,65 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {IERC6538Registry} from "./interfaces/IERC6538Registry.sol";
garyghayrat marked this conversation as resolved.
Show resolved Hide resolved

/// @dev `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.
/// @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 stealthMetaAddress)) public
stealthMetaAddressOf;
mds1 marked this conversation as resolved.
Show resolved Hide resolved

/// @notice A nonce used to ensure a signature can only be used once.
/// @dev `user` is the registrant address.
/// @dev `nonce` will be incremented after each valid `registerKeysOnBehalf` call.
mapping(address user => uint256 nonce) public nonceOf;
mds1 marked this conversation as resolved.
Show resolved Hide resolved

/// @dev EIP-712 Type hash used in `registerKeysOnBehalf`.
bytes32 public constant TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address registryContract)");
garyghayrat marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Cache the domain separator as an immutable value.
bytes32 private immutable _cachedDomainSeparator;
/// @dev Store the chain id that `_cachedDomainSeparator` corresponds to, in order to invalidate
/// the cached domain separator if the chain id changes.
uint256 private immutable _cachedChainId;
garyghayrat marked this conversation as resolved.
Show resolved Hide resolved
/// @dev Cache the address of this contract as an immutable value.
address private immutable _cachedThis;
apbendi marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Cache the hash of the name as an immutable value.
bytes32 private immutable _hashedName;
/// @dev Cache the hash of the version as immutable value.
bytes32 private immutable _hashedVersion;
apbendi marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Name of the registry.
string private _name;
/// @dev Version of the registry.
string private _version;
apbendi marked this conversation as resolved.
Show resolved Hide resolved

enum RecoverError {
mds1 marked this conversation as resolved.
Show resolved Hide resolved
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS
}

constructor() {
_name = "ERC6538Registry";
_version = "1";
_hashedName = keccak256(bytes(_name));
_hashedVersion = keccak256(bytes(_version));
_cachedChainId = block.chainid;
_cachedDomainSeparator = _buildDomainSeparator();
_cachedThis = address(this);
}

/// @inheritdoc IERC6538Registry
function registerKeys(uint256 schemeId, bytes memory stealthMetaAddress) external {
mds1 marked this conversation as resolved.
Show resolved Hide resolved
bytes memory registrant = _toBytes(msg.sender);
stealthMetaAddressOf[registrant][schemeId] = stealthMetaAddress;
emit StealthMetaAddressSet(registrant, schemeId, stealthMetaAddress);
stealthMetaAddressOf[msg.sender][schemeId] = stealthMetaAddress;
emit StealthMetaAddressSet(msg.sender, schemeId, stealthMetaAddress);
}

/// @inheritdoc IERC6538Registry
Expand All @@ -24,22 +68,179 @@
uint256 schemeId,
bytes memory signature,
bytes memory stealthMetaAddress
) external pure {
registerKeysOnBehalf(_toBytes(registrant), schemeId, signature, stealthMetaAddress);
) external {
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(TYPE_HASH, registrant, schemeId, stealthMetaAddress, nonceOf[registrant]++)
)
);
require(isValidSignatureNow(registrant, digest, signature), "Invalid signature");
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 {
mds1 marked this conversation as resolved.
Show resolved Hide resolved
nonceOf[msg.sender]++;
mds1 marked this conversation as resolved.
Show resolved Hide resolved
}

/// @notice Returns the domain separator for the current chain.
/// @dev The following code is from OpenZeppelin's `EIP712.sol` file.
/// @dev The method visibility was changed from internal to public. Original file permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/EIP712.sol
function _domainSeparatorV4() public view returns (bytes32) {
if (address(this) == _cachedThis && block.chainid == _cachedChainId) {
return _cachedDomainSeparator;
} else {
return _buildDomainSeparator();
}
}

/// @dev Converts an `address` to `bytes`.
function _toBytes(address who) internal pure returns (bytes memory) {
return bytes.concat(bytes32(uint256(uint160(who))));
/// @notice Returns the hash of the EIP712 Domain Separator.
/// @dev The following code is from OpenZeppelin's `EIP712.sol` file. Permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/EIP712.sol
function _buildDomainSeparator() private view returns (bytes32) {
return
keccak256(abi.encode(TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this)));
}

/// @notice Returns the hash of the fully encoded EIP712 message for this domain.
/// @dev The following code is from OpenZeppelin's `EIP712.sol` file. Permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/EIP712.sol
/// @param structHash The hash of the struct containing the message data, as defined in
/// https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct
function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) {
return toTypedDataHash(_domainSeparatorV4(), structHash);
}
mds1 marked this conversation as resolved.
Show resolved Hide resolved

/// @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`).
/// @dev The digest is calculated from a `domainSeparator` and a `structHash`, by prefixing them
/// with `\x19\x01` and hashing the result. It corresponds to the hash signed by the
/// https://eips.ethereum.org/EIPS/eip-712[`eth_signTypedData`] JSON-RPC method as part of
/// EIP-712.
/// @dev The following code is from OpenZeppelin's `MessageHashUtils.sol` file. Permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/MessageHashUtils.sol
/// @param domainSeparator The domain separator returned by `_domainSeparatorV4`.
/// @param structHash The hash of the struct containing the message data, as defined in
/// https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct
function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash)
internal
pure
returns (bytes32 digest)
{
/// @solidity memory-safe-assembly
assembly {
mds1 marked this conversation as resolved.
Show resolved Hide resolved
let ptr := mload(0x40)
mstore(ptr, hex"1901")
mstore(add(ptr, 0x02), domainSeparator)
mstore(add(ptr, 0x22), structHash)
digest := keccak256(ptr, 0x42)
}
}
Fixed Show fixed Hide fixed

/// @notice Checks if a signature is valid for a given signer and data hash. If the signer is a
/// smart contract, the signature is validated against that smart contract using ERC1271,
/// otherwise it's validated using `tryRecover`.
/// @dev The following code is from OpenZeppelin's `SignatureChecker.sol` file. Permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/SignatureChecker.sol
/// @param signer The address that should have signed the message data.
/// @param hash The digest of message data.
/// @param signature The signature provided by the registrant.
function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature)
internal
view
returns (bool)
{
(address recovered, RecoverError error,) = tryRecover(hash, signature);
return (error == RecoverError.NoError && recovered == signer)
|| isValidERC1271SignatureNow(signer, hash, signature);
}

/// @notice Checks if a signature is valid for a given signer and data hash. The signature is
/// validated against the signer smart contract using ERC1271.
/// @dev The following code is from OpenZeppelin's `SignatureChecker.sol` file. Permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/SignatureChecker.sol
/// @param signer The address that should have signed the message data.
/// @param hash The digest of message data.
/// @param signature The signature provided by the registrant.
function isValidERC1271SignatureNow(address signer, bytes32 hash, bytes memory signature)
internal
view
returns (bool)
{
(bool success, bytes memory result) =
signer.staticcall(abi.encodeCall(IERC1271.isValidSignature, (hash, signature)));
garyghayrat marked this conversation as resolved.
Show resolved Hide resolved
return (
success && result.length >= 32
&& abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)
);
}
Fixed Show fixed Hide fixed

/// @dev The following code is from OpenZeppelin's `ECDSA.sol` file. Permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/ECDSA.sol
/// @param hash The digest of message data.
/// @param signature The signature provided by the registrant.
function tryRecover(bytes32 hash, bytes memory signature)
internal
pure
returns (address, RecoverError, bytes32)
{
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
// ecrecover takes the signature parameters, and the only way to get them currently is to use
// assembly.
assembly ("memory-safe") {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else {
return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length));
}
}

/// @notice Recover the signer's address using `v`, `r` and `s` signature fields.
/// @dev The following code is from OpenZeppelin's `ECDSA.sol` file. Permalink:
/// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e70a0118ef10773457f670671baefad2c5ea610d/contracts/utils/cryptography/ECDSA.sol
function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)
internal
pure
returns (address, RecoverError, bytes32)
{
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make
// the signature unique. Appendix F in the Ethereum Yellow paper
// (https://ethereum.github.io/yellowpaper/paper.pdf), defines the valid range for s in (301):
// 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most signatures from
// current libraries generate a unique signature with an s-value in the lower half order.

// If your library generates malleable signatures, such as s-values in the upper range,
// calculate a new s-value with
// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27
// to 28 or vice versa. If your library also generates signatures with 0/1 for v instead 27/28,
// add 27 to v to accept these malleable signatures as well.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS, s);
}

// If the signature is valid (and not malleable), return the signer address
address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) return (address(0), RecoverError.InvalidSignature, bytes32(0));

return (signer, RecoverError.NoError, bytes32(0));
}
}

/// @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 {
/// @dev Should return whether the signature provided is valid for the provided data
/// @param hash Hash of the data to be signed
nerolation marked this conversation as resolved.
Show resolved Hide resolved
/// @param signature Signature byte array associated with _data
function isValidSignature(bytes32 hash, bytes memory signature)
external
view
returns (bytes4 magicValue);
}
4 changes: 2 additions & 2 deletions src/interfaces/IERC5564Announcer.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// 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
Expand Down
21 changes: 3 additions & 18 deletions src/interfaces/IERC6538Registry.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

garyghayrat marked this conversation as resolved.
Show resolved Hide resolved
/// @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.
Expand All @@ -16,7 +16,7 @@ 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.
Expand All @@ -39,19 +39,4 @@ interface IERC6538Registry {
bytes memory signature,
bytes memory 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;
}
Loading
Loading