generated from ScopeLift/foundry-template
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
145d7f2
commit 8131727
Showing
10 changed files
with
619 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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/[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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.