diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 25fa6215..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "typescript.tsdk": "node_modules/typescript/lib" -} diff --git a/contracts/reverseRegistrar/IL2ReverseRegistrar.sol b/contracts/reverseRegistrar/IL2ReverseRegistrar.sol new file mode 100644 index 00000000..019b6223 --- /dev/null +++ b/contracts/reverseRegistrar/IL2ReverseRegistrar.sol @@ -0,0 +1,63 @@ +pragma solidity >=0.8.4; + +interface IL2ReverseRegistrar { + function setName(string memory name) external returns (bytes32); + + function setNameForAddr( + address addr, + string memory name + ) external returns (bytes32); + + function setNameForAddrWithSignature( + address addr, + string memory name, + uint256 inceptionDate, + bytes memory signature + ) external returns (bytes32); + + function setNameForAddrWithSignatureAndOwnable( + address contractAddr, + address owner, + string memory name, + uint256 inceptionDate, + bytes memory signature + ) external returns (bytes32); + + function setText( + string calldata key, + string calldata value + ) external returns (bytes32); + + function setTextForAddr( + address addr, + string calldata key, + string calldata value + ) external returns (bytes32); + + function setTextForAddrWithSignature( + address addr, + string calldata key, + string calldata value, + uint256 inceptionDate, + bytes memory signature + ) external returns (bytes32); + + function setTextForAddrWithSignatureAndOwnable( + address contractAddr, + address owner, + string calldata key, + string calldata value, + uint256 inceptionDate, + bytes memory signature + ) external returns (bytes32); + + function clearRecords(address addr) external; + + function clearRecordsWithSignature( + address addr, + uint256 inceptionDate, + bytes memory signature + ) external; + + function node(address addr) external view returns (bytes32); +} diff --git a/contracts/reverseRegistrar/L2ReverseRegistrar.sol b/contracts/reverseRegistrar/L2ReverseRegistrar.sol new file mode 100644 index 00000000..35b3d1f2 --- /dev/null +++ b/contracts/reverseRegistrar/L2ReverseRegistrar.sol @@ -0,0 +1,519 @@ +pragma solidity >=0.8.4; + +import "../registry/ENS.sol"; +import "./IL2ReverseRegistrar.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../resolvers/profiles/ITextResolver.sol"; +import "../resolvers/profiles/INameResolver.sol"; +import "../root/Controllable.sol"; +import "../resolvers/Multicallable.sol"; + +error InvalidSignature(); +error SignatureOutOfDate(); +error Unauthorised(); +error NotOwnerOfContract(); + +// @note Inception date +// The inception date is in milliseconds, and so will be divided by 1000 +// when comparing to block.timestamp. This means that the date will be +// rounded down to the nearest second. + +contract L2ReverseRegistrar is + Multicallable, + Ownable, + ITextResolver, + INameResolver, + IL2ReverseRegistrar +{ + using ECDSA for bytes32; + mapping(bytes32 => uint256) public lastUpdated; + mapping(uint64 => mapping(bytes32 => mapping(string => string))) versionable_texts; + mapping(uint64 => mapping(bytes32 => string)) versionable_names; + mapping(bytes32 => uint64) internal recordVersions; + event VersionChanged(bytes32 indexed node, uint64 newVersion); + event ReverseClaimed(address indexed addr, bytes32 indexed node); + + bytes32 public immutable L2ReverseNode; + uint256 public immutable coinType; + + // This is the hex encoding of the string 'abcdefghijklmnopqrstuvwxyz' + // It is used as a constant to lookup the characters of the hex address + bytes32 constant lookup = + 0x3031323334353637383961626364656600000000000000000000000000000000; + + /** + * @dev Constructor + */ + constructor(bytes32 _L2ReverseNode, uint256 _coinType) { + L2ReverseNode = _L2ReverseNode; + coinType = _coinType; + } + + modifier authorised(address addr) { + isAuthorised(addr); + _; + } + + modifier authorisedSignature( + bytes32 hash, + address addr, + uint256 inceptionDate, + bytes memory signature + ) { + isAuthorisedWithSignature(hash, addr, inceptionDate, signature); + _; + } + + modifier ownerAndAuthorisedWithSignature( + bytes32 hash, + address addr, + address owner, + uint256 inceptionDate, + bytes memory signature + ) { + isOwnerAndAuthorisedWithSignature( + hash, + addr, + owner, + inceptionDate, + signature + ); + _; + } + + function isAuthorised(address addr) internal view returns (bool) { + if (addr != msg.sender && !ownsContract(addr, msg.sender)) { + revert Unauthorised(); + } + } + + function isAuthorisedWithSignature( + bytes32 hash, + address addr, + uint256 inceptionDate, + bytes memory signature + ) internal view returns (bool) { + bytes32 message = keccak256( + abi.encodePacked(hash, addr, inceptionDate, coinType) + ).toEthSignedMessageHash(); + bytes32 node = _getNamehash(addr); + + if (!SignatureChecker.isValidSignatureNow(addr, message, signature)) { + revert InvalidSignature(); + } + + if ( + inceptionDate <= lastUpdated[node] || // must be newer than current record + inceptionDate / 1000 >= block.timestamp // must be in the past + ) { + revert SignatureOutOfDate(); + } + } + + function isOwnerAndAuthorisedWithSignature( + bytes32 hash, + address addr, + address owner, + uint256 inceptionDate, + bytes memory signature + ) internal view returns (bool) { + bytes32 message = keccak256( + abi.encodePacked(hash, addr, owner, inceptionDate, coinType) + ).toEthSignedMessageHash(); + bytes32 node = _getNamehash(addr); + + if (!ownsContract(addr, owner)) { + revert NotOwnerOfContract(); + } + + if ( + !SignatureChecker.isValidERC1271SignatureNow( + owner, + message, + signature + ) + ) { + revert InvalidSignature(); + } + + if ( + inceptionDate <= lastUpdated[node] || // must be newer than current record + inceptionDate / 1000 >= block.timestamp // must be in the past + ) { + revert SignatureOutOfDate(); + } + } + + /** + * @dev Sets the name for an addr using a signature that can be verified with ERC1271. + * @param addr The reverse record to set + * @param name The name of the reverse record + * @param inceptionDate Date from when this signature is valid from + * @param signature The resolver of the reverse node + * @return The ENS node hash of the reverse record. + */ + function setNameForAddrWithSignature( + address addr, + string memory name, + uint256 inceptionDate, + bytes memory signature + ) + public + override + authorisedSignature( + keccak256( + abi.encodePacked( + IL2ReverseRegistrar.setNameForAddrWithSignature.selector, + name + ) + ), + addr, + inceptionDate, + signature + ) + returns (bytes32) + { + bytes32 node = _getNamehash(addr); + + _setName(node, name, inceptionDate); + return node; + } + + /** + * @dev Sets the name for a contract that is owned by a SCW using a signature + * @param contractAddr The reverse node to set + * @param owner The owner of the contract (via Ownable) + * @param name The name of the reverse record + * @param inceptionDate Date from when this signature is valid from + * @param signature The signature of an address that will return true on isValidSignature for the owner + * @return The ENS node hash of the reverse record. + */ + function setNameForAddrWithSignatureAndOwnable( + address contractAddr, + address owner, + string memory name, + uint256 inceptionDate, + bytes memory signature + ) + public + ownerAndAuthorisedWithSignature( + keccak256( + abi.encodePacked( + IL2ReverseRegistrar + .setNameForAddrWithSignatureAndOwnable + .selector, + name + ) + ), + contractAddr, + owner, + inceptionDate, + signature + ) + returns (bytes32) + { + bytes32 node = _getNamehash(contractAddr); + _setName(node, name, inceptionDate); + } + + /** + * @dev Sets the `name()` record for the reverse ENS record associated with + * the calling account. + * @param name The name to set for this address. + * @return The ENS node hash of the reverse record. + */ + function setName(string memory name) public override returns (bytes32) { + return setNameForAddr(msg.sender, name); + } + + /** + * @dev Sets the `name()` record for the reverse ENS record associated with + * the addr provided account. + * Can be used if the addr is a contract that is owned by a SCW. + * @param name The name to set for this address. + * @return The ENS node hash of the reverse record. + */ + + function setNameForAddr( + address addr, + string memory name + ) public authorised(addr) returns (bytes32) { + bytes32 node = _getNamehash(addr); + _setName(node, name, block.timestamp); + return node; + } + + /** + * @dev Sets the name for an addr using a signature that can be verified with ERC1271. + * @param addr The reverse record to set + * @param key The key of the text record + * @param value The value of the text record + * @param inceptionDate Date from when this signature is valid from + * @param signature The resolver of the reverse node + * @return The ENS node hash of the reverse record. + */ + function setTextForAddrWithSignature( + address addr, + string calldata key, + string calldata value, + uint256 inceptionDate, + bytes memory signature + ) + public + override + authorisedSignature( + keccak256( + abi.encodePacked( + IL2ReverseRegistrar.setTextForAddrWithSignature.selector, + key, + value + ) + ), + addr, + inceptionDate, + signature + ) + returns (bytes32) + { + bytes32 node = _getNamehash(addr); + _setText(node, key, value, inceptionDate); + return node; + } + + /** + * @dev Sets the name for a contract that is owned by a SCW using a signature + * @param contractAddr The reverse node to set + * @param owner The owner of the contract (via Ownable) + * @param key The name of the reverse record + * @param value The name of the reverse record + * @param inceptionDate Date from when this signature is valid from + * @param signature The signature of an address that will return true on isValidSignature for the owner + * @return The ENS node hash of the reverse record. + */ + function setTextForAddrWithSignatureAndOwnable( + address contractAddr, + address owner, + string calldata key, + string calldata value, + uint256 inceptionDate, + bytes memory signature + ) + public + ownerAndAuthorisedWithSignature( + keccak256( + abi.encodePacked( + IL2ReverseRegistrar + .setTextForAddrWithSignatureAndOwnable + .selector, + key, + value + ) + ), + contractAddr, + owner, + inceptionDate, + signature + ) + returns (bytes32) + { + bytes32 node = _getNamehash(contractAddr); + _setText(node, key, value, inceptionDate); + } + + /** + * @dev Sets the `name()` record for the reverse ENS record associated with + * the calling account. + * @param key The key for this text record. + * @param value The value to set for this text record. + * @return The ENS node hash of the reverse record. + */ + function setText( + string calldata key, + string calldata value + ) public override returns (bytes32) { + return setTextForAddr(msg.sender, key, value); + } + + /** + * @dev Sets the `text(key)` record for the reverse ENS record associated with + * the addr provided account. + * @param key The key for this text record. + * @param value The value to set for this text record. + * @return The ENS node hash of the reverse record. + */ + + function setTextForAddr( + address addr, + string calldata key, + string calldata value + ) public override authorised(addr) returns (bytes32) { + bytes32 node = _getNamehash(addr); + _setText(node, key, value, block.timestamp); + return node; + } + + function _setText( + bytes32 node, + string calldata key, + string calldata value, + uint256 inceptionDate + ) internal { + versionable_texts[recordVersions[node]][node][key] = value; + _setLastUpdated(node, inceptionDate); + emit TextChanged(node, key, key, value); + } + + /** + * Returns the text data associated with an ENS node and key. + * @param node The ENS node to query. + * @param key The text data key to query. + * @return The associated text data. + */ + function text( + bytes32 node, + string calldata key + ) external view virtual override returns (string memory) { + return versionable_texts[recordVersions[node]][node][key]; + } + + /** + * Sets the name associated with an ENS node, for reverse records. + * May only be called by the owner of that node in the ENS registry. + * @param node The node to update. + * @param newName name record + */ + function _setName( + bytes32 node, + string memory newName, + uint256 inceptionDate + ) internal virtual { + versionable_names[recordVersions[node]][node] = newName; + _setLastUpdated(node, inceptionDate); + emit NameChanged(node, newName); + } + + /** + * Returns the name associated with an ENS node, for reverse records. + * Defined in EIP181. + * @param node The ENS node to query. + * @return The associated name. + */ + function name( + bytes32 node + ) external view virtual override returns (string memory) { + return versionable_names[recordVersions[node]][node]; + } + + /** + * Increments the record version associated with an ENS node. + * May only be called by the owner of that node in the ENS registry. + * @param addr The node to update. + */ + function clearRecords(address addr) public virtual authorised(addr) { + bytes32 labelHash = sha3HexAddress(addr); + bytes32 reverseNode = keccak256( + abi.encodePacked(L2ReverseNode, labelHash) + ); + recordVersions[reverseNode]++; + emit VersionChanged(reverseNode, recordVersions[reverseNode]); + } + + /** + * Increments the record version associated with an ENS node. + * May only be called by the owner of that node in the ENS registry. + * @param addr The node to update. + * @param signature A signature proving ownership of the node. + */ + function clearRecordsWithSignature( + address addr, + uint256 inceptionDate, + bytes memory signature + ) + public + virtual + authorisedSignature( + keccak256( + abi.encodePacked( + IL2ReverseRegistrar.clearRecordsWithSignature.selector + ) + ), + addr, + inceptionDate, + signature + ) + { + bytes32 labelHash = sha3HexAddress(addr); + bytes32 reverseNode = keccak256( + abi.encodePacked(L2ReverseNode, labelHash) + ); + recordVersions[reverseNode]++; + emit VersionChanged(reverseNode, recordVersions[reverseNode]); + } + + /** + * @dev Returns the node hash for a given account's reverse records. + * @param addr The address to hash + * @return The ENS node hash. + */ + function node(address addr) public view override returns (bytes32) { + return keccak256(abi.encodePacked(L2ReverseNode, sha3HexAddress(addr))); + } + + function ownsContract( + address contractAddr, + address addr + ) internal view returns (bool) { + try Ownable(contractAddr).owner() returns (address owner) { + return owner == addr; + } catch { + return false; + } + } + + function _getNamehash(address addr) internal view returns (bytes32) { + bytes32 labelHash = sha3HexAddress(addr); + return keccak256(abi.encodePacked(L2ReverseNode, labelHash)); + } + + function _setLastUpdated(bytes32 node, uint256 inceptionDate) internal { + lastUpdated[node] = inceptionDate; + } + + /** + * @dev An optimised function to compute the sha3 of the lower-case + * hexadecimal representation of an Ethereum address. + * @param addr The address to hash + * @return ret The SHA3 hash of the lower-case hexadecimal encoding of the + * input address. + */ + function sha3HexAddress(address addr) internal pure returns (bytes32 ret) { + assembly { + for { + let i := 40 + } gt(i, 0) { + + } { + i := sub(i, 1) + mstore8(i, byte(and(addr, 0xf), lookup)) + addr := div(addr, 0x10) + i := sub(i, 1) + mstore8(i, byte(and(addr, 0xf), lookup)) + addr := div(addr, 0x10) + } + + ret := keccak256(0, 40) + } + } + + function supportsInterface( + bytes4 interfaceID + ) public view override(Multicallable) returns (bool) { + return + interfaceID == type(IL2ReverseRegistrar).interfaceId || + interfaceID == type(ITextResolver).interfaceId || + interfaceID == type(INameResolver).interfaceId || + super.supportsInterface(interfaceID); + } +} diff --git a/contracts/reverseRegistrar/README.md b/contracts/reverseRegistrar/README.md new file mode 100644 index 00000000..050a50ee --- /dev/null +++ b/contracts/reverseRegistrar/README.md @@ -0,0 +1,25 @@ +# L2 Reverse Registrar + +## Summary + +The L2 Reverse registrar is a combination of a resolver and a reverse registrar that allows the name to be set for a particular reverse node. + +## Inception Date + +Inception date is set in milliseconds, so needs to be divided by 1000 to be compared to block.timestamp which is in seconds + +## Setting records + +You can set records using one of the follow functions: + +`setName()`/`setText()` - uses the msg.sender's address and allows you to set a record for that address only + +`setNameForAddr()`/`setTextForAddr()` - uses the address parameter instead of `msg.sender` and checks if the `msg.sender` is authorised by checking if the contract's owner (via the Ownable pattern) is the msg.sender + +`setNameForAddrWithSignature()`/`setTextForAddrWithSignature()` - uses the address parameter instead of `msg.sender` and allows authorisation via a signature + +`setNameForAddrWithSignatureAndOwnable()`/`setTextForAddrWithSignatureOwnable()` - uses the address parameter instead of `msg.sender`. The sender is authorised by checking if the contract's owner (via the Ownable pattern) is the msg.sender, which then checks that the signer has authorised the record on behalf of msg.sender using `ERC1271` + +## Signatures for setting records + +Signatures use a double keccak256 structure that first hashes the function id with the variables that you are passing to that function. It then hashes the result of that with the rest of the parameters. diff --git a/deploy/registry/01_deploy_reverse_registrar.ts b/deploy/reverseregistrar/00_deploy_reverse_registrar.ts similarity index 100% rename from deploy/registry/01_deploy_reverse_registrar.ts rename to deploy/reverseregistrar/00_deploy_reverse_registrar.ts diff --git a/deploy/reverseregistrar/01_deploy_l2_reverse_registrar.ts b/deploy/reverseregistrar/01_deploy_l2_reverse_registrar.ts new file mode 100644 index 00000000..83f4793e --- /dev/null +++ b/deploy/reverseregistrar/01_deploy_l2_reverse_registrar.ts @@ -0,0 +1,25 @@ +import { namehash } from 'ethers/lib/utils' +import { ethers } from 'hardhat' +import { DeployFunction } from 'hardhat-deploy/types' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +// Replace with coinid of L2 +const COINTYPE = 123 + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { getNamedAccounts, deployments, network } = hre + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + await deploy('L2ReverseRegistrar', { + from: deployer, + args: [namehash(`${COINTYPE}.reverse`), COINTYPE], + log: true, + }) +} + +func.id = 'l2-reverse-registrar' +func.tags = ['L2ReverseRegistrar'] +func.dependencies = [] + +export default func diff --git a/package.json b/package.json index d72f21bb..9489c418 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "dependencies": { "@ensdomains/buffer": "^0.1.1", "@ensdomains/solsha1": "0.0.3", - "@openzeppelin/contracts": "^4.1.0", + "@openzeppelin/contracts": "4.9.3", "dns-packet": "^5.3.0" }, "directories": { diff --git a/test/reverseRegistrar/TestL2ReverseRegistrar.js b/test/reverseRegistrar/TestL2ReverseRegistrar.js new file mode 100644 index 00000000..b207471e --- /dev/null +++ b/test/reverseRegistrar/TestL2ReverseRegistrar.js @@ -0,0 +1,615 @@ +const { expect } = require('chai') +const { ethers } = require('hardhat') +const { namehash } = require('../test-utils/ens') + +const keccak256 = ethers.utils.solidityKeccak256 + +describe('L2ReverseRegistrar', function () { + let L2ReverseRegistrar + let L2ReverseRegistrarWithAccount2 + let MockSmartContractWallet + let MockOwnable + let signers + let account + let account2 + let setNameForAddrWithSignatureFuncSig = + 'setNameForAddrWithSignature(address,string,uint256,bytes)' + let setNameForAddrWithSignatureAndOwnableFuncSig = + 'setNameForAddrWithSignatureAndOwnable(address,address,string,uint256,bytes)' + let setTextForAddrWithSignatureFuncSig = + 'setTextForAddrWithSignature(address,string,string,uint256,bytes)' + let setTextForAddrWithSignatureAndOwnableFuncSig = + 'setTextForAddrWithSignatureAndOwnable(address,address,string,string,uint256,bytes)' + let coinType = 123 + + before(async function () { + signers = await ethers.getSigners() + account = await signers[0].getAddress() + account2 = await signers[1].getAddress() + + const L2ReverseRegistrarFactory = await ethers.getContractFactory( + 'L2ReverseRegistrar', + ) + L2ReverseRegistrar = await L2ReverseRegistrarFactory.deploy( + namehash('optimism.reverse'), + coinType, + ) + + const MockSmartContractWalletFactory = await ethers.getContractFactory( + 'MockSmartContractWallet', + ) + MockSmartContractWallet = await MockSmartContractWalletFactory.deploy( + account, + ) + + const MockOwnableFactory = await ethers.getContractFactory('MockOwnable') + MockOwnable = await MockOwnableFactory.deploy( + MockSmartContractWallet.address, + ) + + L2ReverseRegistrarWithAccount2 = L2ReverseRegistrar.connect(signers[1]) + + await L2ReverseRegistrar.deployed() + }) + + beforeEach(async () => { + result = await ethers.provider.send('evm_snapshot') + }) + afterEach(async () => { + await ethers.provider.send('evm_revert', [result]) + }) + + it('should deploy the contract', async function () { + expect(L2ReverseRegistrar.address).to.not.equal(0) + }) + + describe('setName', () => { + it('should set the name record for the calling account', async function () { + const name = 'myname.eth' + const tx = await L2ReverseRegistrar.setName(name) + await tx.wait() + + const node = await L2ReverseRegistrar.node( + await ethers.provider.getSigner().getAddress(), + ) + const actualName = await L2ReverseRegistrar.name(node) + expect(actualName).to.equal(name) + }) + }) + + describe('setNameForAddrWithSignature', () => { + it('allows an account to sign a message to allow a relayer to claim the address', async () => { + const funcId = ethers.utils + .id(setNameForAddrWithSignatureFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256(['bytes4', 'string'], [funcId, 'hello.eth']), + account, + inceptionDate, + coinType, + ], + ), + ), + ) + + await L2ReverseRegistrarWithAccount2['setNameForAddrWithSignature']( + account, + 'hello.eth', + inceptionDate, + signature, + ) + + const node = await L2ReverseRegistrar.node(account) + assert.equal(await L2ReverseRegistrar.name(node), 'hello.eth') + }) + + it('reverts if signature parameters do not match', async () => { + const funcId = ethers.utils + .id(setNameForAddrWithSignatureFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + 3600 + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256'], + [ + keccak256(['bytes4', 'string'], [funcId, 'hello.eth']), + account, + inceptionDate, + ], + ), + ), + ) + + await expect( + L2ReverseRegistrarWithAccount2[setNameForAddrWithSignatureFuncSig]( + account, + 'notthesamename.eth', + inceptionDate, + signature, + ), + ).to.be.revertedWith(`InvalidSignature()`) + }) + + it('reverts if inception date is too low', async () => { + const funcId = ethers.utils + .id(setNameForAddrWithSignatureFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256(['bytes4', 'string'], [funcId, 'hello.eth']), + account, + inceptionDate, + coinType, + ], + ), + ), + ) + + await L2ReverseRegistrarWithAccount2['setNameForAddrWithSignature']( + account, + 'hello.eth', + inceptionDate, + signature, + ) + + const node = await L2ReverseRegistrar.node(account) + assert.equal(await L2ReverseRegistrar.name(node), 'hello.eth') + + const inceptionDate2 = 0 + const signature2 = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256(['bytes4', 'string'], [funcId, 'hello.eth']), + account, + inceptionDate2, + coinType, + ], + ), + ), + ) + + await expect( + L2ReverseRegistrarWithAccount2['setNameForAddrWithSignature']( + account, + 'hello.eth', + inceptionDate2, + signature2, + ), + ).to.be.revertedWith(`SignatureOutOfDate()`) + }) + }) + + describe('setNameForAddrWithSignatureAndOwnable', () => { + it('allows an account to sign a message to allow a relayer to claim the address of a contract that is owned by another contract that the account is a signer of', async () => { + const node = await L2ReverseRegistrar.node(MockOwnable.address) + assert.equal(await L2ReverseRegistrar.name(node), '') + const funcId = ethers.utils + .id(setNameForAddrWithSignatureAndOwnableFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'address', 'uint256', 'uint256'], + [ + keccak256(['bytes4', 'string'], [funcId, 'ownable.eth']), + MockOwnable.address, + MockSmartContractWallet.address, + inceptionDate, + coinType, + ], + ), + ), + ) + + await L2ReverseRegistrarWithAccount2[ + 'setNameForAddrWithSignatureAndOwnable' + ]( + MockOwnable.address, + MockSmartContractWallet.address, + 'ownable.eth', + inceptionDate, + signature, + ) + + assert.equal(await L2ReverseRegistrar.name(node), 'ownable.eth') + }) + }) + + describe('setText', () => { + it('should set the text record for the calling account', async function () { + const key = 'url;' + const value = 'http://ens.domains' + const tx = await L2ReverseRegistrar.setText(key, value) + await tx.wait() + + const node = await L2ReverseRegistrar.node( + await ethers.provider.getSigner().getAddress(), + ) + const actualRecord = await L2ReverseRegistrar.text(node, key) + expect(actualRecord).to.equal(value) + }) + }) + + describe('setTextForAddrWithSignature', function () { + it('allows an account to sign a message to allow a relayer to claim the address', async () => { + const funcId = ethers.utils + .id(setTextForAddrWithSignatureFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256( + ['bytes4', 'string', 'string'], + [funcId, 'url', 'http://ens.domains'], + ), + account, + inceptionDate, + coinType, + ], + ), + ), + ) + + await L2ReverseRegistrarWithAccount2['setTextForAddrWithSignature']( + account, + 'url', + 'http://ens.domains', + inceptionDate, + signature, + ) + + const node = await L2ReverseRegistrar.node(account) + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://ens.domains', + ) + }) + + it('reverts if signature parameters do not match', async () => { + const funcId = ethers.utils + .id(setTextForAddrWithSignatureFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256'], + [ + keccak256( + ['bytes4', 'string', 'string'], + [funcId, 'url', 'http://ens.domains'], + ), + account, + inceptionDate, + ], + ), + ), + ) + + await expect( + L2ReverseRegistrarWithAccount2[setTextForAddrWithSignatureFuncSig]( + account, + 'url', + 'http://some.other.url.com', + inceptionDate, + signature, + ), + ).to.be.revertedWith(`InvalidSignature()`) + }) + + it('reverts if inception date is too low', async () => { + const funcId = ethers.utils + .id(setTextForAddrWithSignatureFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256( + ['bytes4', 'string', 'string'], + [funcId, 'url', 'http://ens.domains'], + ), + account, + inceptionDate, + coinType, + ], + ), + ), + ) + + await L2ReverseRegistrarWithAccount2['setTextForAddrWithSignature']( + account, + 'url', + 'http://ens.domains', + inceptionDate, + signature, + ) + + const node = await L2ReverseRegistrar.node(account) + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://ens.domains', + ) + + const inceptionDate2 = 0 + const signature2 = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256( + ['bytes4', 'string', 'string'], + [funcId, 'url', 'http://ens.domains'], + ), + account, + inceptionDate2, + coinType, + ], + ), + ), + ) + + await expect( + L2ReverseRegistrarWithAccount2['setTextForAddrWithSignature']( + account, + 'url', + 'http://ens.domains', + inceptionDate2, + signature2, + ), + ).to.be.revertedWith(`SignatureOutOfDate()`) + }) + }) + + describe('setTextForAddrWithSignatureAndOwnable', function () { + it('allows an account to sign a message to allow a relayer to claim the address of a contract that is owned by another contract that the account is a signer of', async () => { + const node = await L2ReverseRegistrar.node(MockOwnable.address) + assert.equal(await L2ReverseRegistrar.text(node, 'url'), '') + const funcId = ethers.utils + .id(setTextForAddrWithSignatureAndOwnableFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'address', 'uint256', 'uint256'], + [ + keccak256( + ['bytes4', 'string', 'string'], + [funcId, 'url', 'http://ens.domains'], + ), + MockOwnable.address, + MockSmartContractWallet.address, + inceptionDate, + coinType, + ], + ), + ), + ) + + await L2ReverseRegistrarWithAccount2[ + 'setTextForAddrWithSignatureAndOwnable' + ]( + MockOwnable.address, + MockSmartContractWallet.address, + 'url', + 'http://ens.domains', + inceptionDate, + signature, + ) + + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://ens.domains', + ) + }) + }) + + describe('Multicallable', function () { + it('setText() + setName()', async () => { + const node = await L2ReverseRegistrar.node(account) + + const calls = [ + L2ReverseRegistrar.interface.encodeFunctionData('setText', [ + 'url', + 'http://multicall.xyz', + ]), + L2ReverseRegistrar.interface.encodeFunctionData('setName', [ + 'hello.eth', + ]), + ] + + await L2ReverseRegistrar.multicall(calls) + + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://multicall.xyz', + ) + + assert.equal(await L2ReverseRegistrar.name(node), 'hello.eth') + }) + + it('setTextForAddrWithSignature()', async () => { + const node = await L2ReverseRegistrar.node(account) + assert.equal(await L2ReverseRegistrar.text(node, 'randomKey'), '') + const funcId1 = ethers.utils + .id(setTextForAddrWithSignatureFuncSig) + .substring(0, 10) + + const funcId2 = ethers.utils + .id(setNameForAddrWithSignatureFuncSig) + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp + + const signature1 = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256( + ['bytes4', 'string', 'string'], + [funcId1, 'url', 'http://ens.domains'], + ), + account, + inceptionDate, + coinType, + ], + ), + ), + ) + + const signature2 = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [ + keccak256(['bytes4', 'string'], [funcId2, 'hello.eth']), + account, + inceptionDate + 1, + coinType, + ], + ), + ), + ) + + const calls = [ + L2ReverseRegistrar.interface.encodeFunctionData( + 'setTextForAddrWithSignature', + [account, 'url', 'http://ens.domains', inceptionDate, signature1], + ), + L2ReverseRegistrar.interface.encodeFunctionData( + 'setNameForAddrWithSignature', + [account, 'hello.eth', inceptionDate + 1, signature2], + ), + ] + + await L2ReverseRegistrar.multicall(calls) + + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://ens.domains', + ) + + assert.equal(await L2ReverseRegistrar.name(node), 'hello.eth') + }) + }) + describe('Clear records', function () { + it('clearRecords() clears records', async () => { + const node = await L2ReverseRegistrar.node(account) + await L2ReverseRegistrar.setText('url', 'http://ens.domains') + await L2ReverseRegistrar.setName('hello.eth') + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://ens.domains', + ) + assert.equal(await L2ReverseRegistrar.name(node), 'hello.eth') + await L2ReverseRegistrar.clearRecords(account) + assert.equal(await L2ReverseRegistrar.text(node, 'url'), '') + assert.equal(await L2ReverseRegistrar.name(node), '') + }) + + it('clearRecordsWithSignature() clears records', async () => { + const node = await L2ReverseRegistrar.node(account) + await L2ReverseRegistrar.setText('url', 'http://ens.domains') + await L2ReverseRegistrar.setName('hello.eth') + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://ens.domains', + ) + assert.equal(await L2ReverseRegistrar.name(node), 'hello.eth') + + const funcId = ethers.utils + .id('clearRecordsWithSignature(address,uint256,bytes)') + .substring(0, 10) + + const block = await ethers.provider.getBlock('latest') + const inceptionDate = block.timestamp * 1000 + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [keccak256(['bytes4'], [funcId]), account, inceptionDate, coinType], + ), + ), + ) + + await L2ReverseRegistrarWithAccount2['clearRecordsWithSignature']( + account, + inceptionDate, + signature, + ) + + assert.equal(await L2ReverseRegistrar.text(node, 'url'), '') + assert.equal(await L2ReverseRegistrar.name(node), '') + }) + + it('clearRecordsWithSignature() reverts when signature expiry is too low', async () => { + const node = await L2ReverseRegistrar.node(account) + await L2ReverseRegistrar.setText('url', 'http://ens.domains') + await L2ReverseRegistrar.setName('hello.eth') + assert.equal( + await L2ReverseRegistrar.text(node, 'url'), + 'http://ens.domains', + ) + assert.equal(await L2ReverseRegistrar.name(node), 'hello.eth') + + const funcId = ethers.utils + .id('clearRecordsWithSignature(address,uint256,bytes)') + .substring(0, 10) + + const inceptionDate = 0 + const signature = await signers[0].signMessage( + ethers.utils.arrayify( + keccak256( + ['bytes32', 'address', 'uint256', 'uint256'], + [keccak256(['bytes4'], [funcId]), account, inceptionDate, coinType], + ), + ), + ) + + await expect( + L2ReverseRegistrarWithAccount2['clearRecordsWithSignature']( + account, + inceptionDate, + signature, + ), + ).to.be.revertedWith(`SignatureOutOfDate()`) + }) + }) +}) diff --git a/test/reverseRegistrar/mocks/MockOwnable.sol b/test/reverseRegistrar/mocks/MockOwnable.sol new file mode 100644 index 00000000..c0354666 --- /dev/null +++ b/test/reverseRegistrar/mocks/MockOwnable.sol @@ -0,0 +1,10 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.17 <0.9.0; + +contract MockOwnable { + address public owner; + + constructor(address _owner) { + owner = _owner; + } +} diff --git a/test/reverseRegistrar/mocks/MockSmartContractWallet.sol b/test/reverseRegistrar/mocks/MockSmartContractWallet.sol new file mode 100644 index 00000000..53250984 --- /dev/null +++ b/test/reverseRegistrar/mocks/MockSmartContractWallet.sol @@ -0,0 +1,22 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.17 <0.9.0; +// import signatureVerifier by openzepellin +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; + +contract MockSmartContractWallet { + address public owner; + + constructor(address _owner) { + owner = _owner; + } + + function isValidSignature( + bytes32 hash, + bytes memory signature + ) public view returns (bytes4) { + if (SignatureChecker.isValidSignatureNow(owner, hash, signature)) { + return 0x1626ba7e; + } + return 0xffffffff; + } +} diff --git a/yarn.lock b/yarn.lock index 91ac4da1..2c00c781 100644 --- a/yarn.lock +++ b/yarn.lock @@ -865,10 +865,10 @@ find-up "^4.1.0" fs-extra "^8.1.0" -"@openzeppelin/contracts@^4.1.0": - version "4.8.0" - resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.8.0.tgz#6854c37df205dd2c056bdfa1b853f5d732109109" - integrity sha512-AGuwhRRL+NaKx73WKRNzeCxOCOCxpaqF+kp8TJ89QzAipSwZy/NoflkWaL9bywXFRhIzXt8j38sfF7KBKCPWLw== +"@openzeppelin/contracts@4.9.3": + version "4.9.3" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.3.tgz#00d7a8cf35a475b160b3f0293a6403c511099364" + integrity sha512-He3LieZ1pP2TNt5JbkPA4PNT9WC3gOTOlDcFGJW4Le4QKqwmiNJCRt44APfxMxvq7OugU/cqYuPcSBzOw38DAg== "@openzeppelin/test-helpers@^0.5.11": version "0.5.16"