Skip to content

Commit

Permalink
Update contracts and tests
Browse files Browse the repository at this point in the history
* Add Create2 deploy script

* Add signature verification

* Add nonce tracking

* Change contract licenses to MIT

* Add natspec and `incrementNonce` method

* Update solidity version

* Increase test coverage

* Add CI action to check interface is in sync w/ contract

* Improvements to tests

---------

Co-authored-by: Ben DiFrancesco <[email protected]>
  • Loading branch information
garyghayrat and apbendi authored Feb 6, 2024
1 parent 145d7f2 commit 8131727
Show file tree
Hide file tree
Showing 10 changed files with 619 additions and 93 deletions.
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -110,7 +131,7 @@ jobs:
- uses: actions/checkout@v3

- name: Run Slither
uses: crytic/[email protected].0
uses: crytic/[email protected].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.
Expand Down
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
20 changes: 16 additions & 4 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -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";
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");
}
}
40 changes: 32 additions & 8 deletions src/ERC5564Announcer.sol
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
166 changes: 138 additions & 28 deletions src/ERC6538Registry.sol
Original file line number Diff line number Diff line change
@@ -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:<shortName>:0x<spendingPubKey>:<viewingPubKey>
/// 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);
}
16 changes: 8 additions & 8 deletions src/interfaces/IERC5564Announcer.sol
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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(
Expand Down
Loading

0 comments on commit 8131727

Please sign in to comment.