diff --git a/.circleci/config.yml b/.circleci/config.yml index a959d44..e4b70d7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,6 +22,7 @@ jobs: command: | truffle test ./test/fifs/fifs_registrar_rskowner.test.js truffle test ./test/fifs/fifs_registrar.test.js + truffle test ./test/fifs/fifs_addr_registrar.test.js truffle test ./test/nodeowner/nodeowner_erc721.test.js truffle test ./test/nodeowner/nodeowner_reclaim.test.js truffle test ./test/nodeowner/nodeowner_registrar_role.test.js diff --git a/contracts/Dummy.sol b/contracts/Dummy.sol index c796401..d566ff4 100644 --- a/contracts/Dummy.sol +++ b/contracts/Dummy.sol @@ -3,6 +3,7 @@ pragma solidity ^0.5.3; import "@rsksmart/rns-registry/contracts/RNS.sol"; import "@rsksmart/rns-auction-registrar/contracts/TokenRegistrar.sol"; import "@rsksmart/erc677/contracts/ERC677.sol"; +import "@rsksmart/rns-resolver/contracts/PublicResolver.sol"; contract Dummy { } diff --git a/contracts/FIFSAddrRegistrar.sol b/contracts/FIFSAddrRegistrar.sol new file mode 100644 index 0000000..d57848d --- /dev/null +++ b/contracts/FIFSAddrRegistrar.sol @@ -0,0 +1,146 @@ +pragma solidity ^0.5.3; + +import "@rsksmart/rns-registry/contracts/AbstractRNS.sol"; +import "@rsksmart/rns-resolver/contracts/AbstractAddrResolver.sol"; +import "./FIFSRegistrarBase.sol"; +import "./PricedContract.sol"; + +/// @title First-in first-served registrar with automatic addr setup. +/// @notice You can use this contract to register names in RNS with addr +/// resolution set automatcially. +/// @dev This contract has permission to register in Node Owner. +contract FIFSAddrRegistrar is FIFSRegistrarBase, PricedContract { + address pool; + AbstractRNS rns; + bytes32 rootNode; + + // sha3('register(string,address,bytes32,uint,address)') + bytes4 constant REGISTER_SIGNATURE = 0x5f7b99d5; + + constructor ( + ERC677 _rif, + NodeOwner _nodeOwner, + address _pool, + AbstractNamePrice _namePrice, + AbstractRNS _rns, + bytes32 _rootNode + ) public FIFSRegistrarBase(_rif, _nodeOwner) PricedContract(_namePrice) { + pool = _pool; + rns = _rns; + rootNode = _rootNode; + } + + /* + 3. Execute registration via: + - ERC-20 with approve() + register() + - ERC-677 with transferAndCall() + The price of a domain is given by name price contract. + */ + + // - Via ERC-20 + /// @notice Registers a .rsk name in RNS. + /// @dev This method must be called after commiting. + /// @param name The name to register. + /// @param nameOwner The owner of the name to regiter. + /// @param secret The secret used to make the commitment. + /// @param duration Time to register in years. + /// @param addr Address to set as addr resolution. + function register( + string calldata name, + address nameOwner, + bytes32 secret, + uint duration, + address addr + ) external { + uint cost = executeRegistration(name, nameOwner, secret, duration, addr); + require(rif.transferFrom(msg.sender, pool, cost), "Token transfer failed"); + } + + // - Via ERC-677 + /* Encoding: + | signature | 4 bytes - offset 0 + | owner | 20 bytes - offset 4 + | secret | 32 bytes - offest 24 + | duration | 32 bytes - offset 56 + | duration | 20 bytes - offset 88 + | name | variable size - offset 108 + */ + + /// @notice ERC-677 token fallback function. + /// @dev Follow 'Register encoding' to execute a one-transaction regitration. + /// @param from token sender. + /// @param value amount of tokens sent. + /// @param data data associated with transaction. + /// @return true if successfull. + function tokenFallback(address from, uint value, bytes calldata data) external returns (bool) { + require(msg.sender == address(rif), "Only RIF token"); + require(data.length > 108, "Invalid data"); + + bytes4 signature = data.toBytes4(0); + + require(signature == REGISTER_SIGNATURE, "Invalid signature"); + + address nameOwner = data.toAddress(4); + bytes32 secret = data.toBytes32(24); + uint duration = data.toUint(56); + address addr = data.toAddress(88); + string memory name = data.toString(108, data.length.sub(108)); + + registerWithToken(name, nameOwner, secret, duration, from, value, addr); + + return true; + } + + function registerWithToken( + string memory name, + address nameOwner, + bytes32 secret, + uint duration, + address from, + uint amount, + address addr + ) private { + uint cost = executeRegistration(name, nameOwner, secret, duration, addr); + require(amount >= cost, "Not enough tokens"); + require(rif.transfer(pool, cost), "Token transfer failed"); + if (amount.sub(cost) > 0) + require(rif.transfer(from, amount.sub(cost)), "Token transfer failed"); + } + + /// @notice Executes registration abstracted from payment method. + /// @param name The name to register. + /// @param nameOwner The owner of the name to regiter. + /// @param secret The secret used to make the commitment. + /// @param duration Time to register in years. + /// @param addr Address to set as addr resolution. + /// @return price Price of the name to register. + function executeRegistration ( + string memory name, + address nameOwner, + bytes32 secret, + uint duration, + address addr + ) private returns (uint) { + bytes32 label = keccak256(abi.encodePacked(name)); + uint256 tokenId = uint256(label); + + require(name.strlen() >= minLength, "Short names not available"); + + bytes32 commitment = makeCommitment(label, nameOwner, secret); + require(canReveal(commitment), "No commitment found"); + commitmentRevealTime[commitment] = 0; + + nodeOwner.register(label, address(this), duration.mul(365 days)); + + AbstractAddrResolver(rns.resolver(rootNode)) + .setAddr( + keccak256(abi.encodePacked(rootNode, label)), + addr + ); + + nodeOwner.reclaim(tokenId, nameOwner); + nodeOwner.transferFrom(address(this), nameOwner, tokenId); + + return price(name, nodeOwner.expirationTime(uint(label)), duration); + } +} diff --git a/contracts/FIFSRegistrar.sol b/contracts/FIFSRegistrar.sol index f09b1d3..9a0b68a 100644 --- a/contracts/FIFSRegistrar.sol +++ b/contracts/FIFSRegistrar.sol @@ -1,29 +1,12 @@ pragma solidity ^0.5.3; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "@ensdomains/ethregistrar/contracts/StringUtils.sol"; -import "@rsksmart/erc677/contracts/ERC677.sol"; -import "@rsksmart/erc677/contracts/ERC677TransferReceiver.sol"; -import "./NodeOwner.sol"; +import "./FIFSRegistrarBase.sol"; import "./PricedContract.sol"; -import "./AbstractNamePrice.sol"; -import "./BytesUtils.sol"; /// @title First-in first-served registrar. /// @notice You can use this contract to register .rsk names in RNS. /// @dev This contract has permission to register in RSK Owner. -contract FIFSRegistrar is PricedContract, ERC677TransferReceiver { - using SafeMath for uint256; - using StringUtils for string; - using BytesUtils for bytes; - - mapping (bytes32 => uint) private commitmentRevealTime; - uint public minCommitmentAge = 1 minutes; - - uint public minLength = 5; - - ERC677 rif; - NodeOwner nodeOwner; +contract FIFSRegistrar is FIFSRegistrarBase, PricedContract { address pool; // sha3('register(string,address,bytes32,uint)') @@ -34,58 +17,17 @@ contract FIFSRegistrar is PricedContract, ERC677TransferReceiver { NodeOwner _nodeOwner, address _pool, AbstractNamePrice _namePrice - ) public PricedContract(_namePrice) { - rif = _rif; - nodeOwner = _nodeOwner; + ) public FIFSRegistrarBase(_rif, _nodeOwner) PricedContract(_namePrice) { pool = _pool; } - /////////////////// - // COMMIT-REVEAL // - /////////////////// - /* - 0. Caclulate makeCommitment hash of the domain to be registered (off-chain) - 1. Commit the calculated hash - 2. Wait minCommitmentAge 3. Execute registration via: - ERC-20 with approve() + register() - ERC-677 with transferAndCall() - The price of a domain is given by name price contract. + The price of a domain is given by name price contract. */ - // 0. - /// @notice Create a commitment for register action. - /// @dev Don't use this method on-chain when commiting. - /// @param label keccak256 of the name to be registered. - /// @param nameOwner Owner of the name to be registered. - /// @param secret Secret to protect the name to be registered. - /// @return The commitment hash. - function makeCommitment (bytes32 label, address nameOwner, bytes32 secret) public pure returns (bytes32) { - return keccak256(abi.encodePacked(label, nameOwner, secret)); - } - - // 1. - /// @notice Commit before registring a name. - /// @dev A valid commitment can be calculated using makeCommitment off-chain. - /// @param commitment A valid commitment hash. - function commit(bytes32 commitment) external { - require(commitmentRevealTime[commitment] < 1, "Existent commitment"); - commitmentRevealTime[commitment] = now.add(minCommitmentAge); - } - - // 2. - /// @notice Ensure the commitment is ready to be revealed. - /// @dev This method can be polled to ensure registration. - /// @param commitment Commitment to be queried. - /// @return Wether the commitment can be revealed or not. - function canReveal(bytes32 commitment) public view returns (bool) { - uint revealTime = commitmentRevealTime[commitment]; - return 0 < revealTime && revealTime <= now; - } - - // 3. - // - Via ERC-20 /// @notice Registers a .rsk name in RNS. /// @dev This method must be called after commiting. @@ -158,22 +100,4 @@ contract FIFSRegistrar is PricedContract, ERC677TransferReceiver { return price(name, nodeOwner.expirationTime(uint(label)), duration); } - - ///////////////////// - // REGISTRAR ADMIN // - ///////////////////// - - /// @notice Change required commitment maturity. - /// @dev Only owner. - /// @param newMinCommitmentAge The new maturity required. - function setMinCommitmentAge (uint newMinCommitmentAge) external onlyOwner { - minCommitmentAge = newMinCommitmentAge; - } - - /// @notice Change disbaled names. - /// @dev Only owner. - /// @param newMinLength The new minimum length enabled. - function setMinLength (uint newMinLength) external onlyOwner { - minLength = newMinLength; - } } diff --git a/contracts/FIFSRegistrarBase.sol b/contracts/FIFSRegistrarBase.sol new file mode 100644 index 0000000..8d261b6 --- /dev/null +++ b/contracts/FIFSRegistrarBase.sol @@ -0,0 +1,95 @@ +pragma solidity ^0.5.3; + +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@ensdomains/ethregistrar/contracts/StringUtils.sol"; +import "@rsksmart/erc677/contracts/ERC677.sol"; +import "@rsksmart/erc677/contracts/ERC677TransferReceiver.sol"; +import "./NodeOwner.sol"; +import "./AbstractNamePrice.sol"; +import "./BytesUtils.sol"; + + +/// @title First-in first-served registrar base. +/// @notice This is an abstract contract. A Registrar can inherit from +/// this contract to implement basic commit-reveal and admin functionality. +/// @dev Inherited contract should have registrar permission in Node Owner. +contract FIFSRegistrarBase is ERC677TransferReceiver, Ownable { + using SafeMath for uint256; + using StringUtils for string; + using BytesUtils for bytes; + + mapping (bytes32 => uint) internal commitmentRevealTime; + uint public minCommitmentAge = 1 minutes; + + uint public minLength = 5; + + ERC677 rif; + NodeOwner nodeOwner; + + constructor ( + ERC677 _rif, + NodeOwner _nodeOwner + ) public { + rif = _rif; + nodeOwner = _nodeOwner; + } + + /////////////////// + // COMMIT-REVEAL // + /////////////////// + + /* + 0. Caclulate makeCommitment hash of the domain to be registered (off-chain) + 1. Commit the calculated hash + 2. Wait minCommitmentAge + 3. Execute registration via inheriting contract. + */ + + // 0. + /// @notice Create a commitment for register action. + /// @dev Don't use this method on-chain when commiting. + /// @param label keccak256 of the name to be registered. + /// @param nameOwner Owner of the name to be registered. + /// @param secret Secret to protect the name to be registered. + /// @return The commitment hash. + function makeCommitment (bytes32 label, address nameOwner, bytes32 secret) public pure returns (bytes32) { + return keccak256(abi.encodePacked(label, nameOwner, secret)); + } + + // 1. + /// @notice Commit before registring a name. + /// @dev A valid commitment can be calculated using makeCommitment off-chain. + /// @param commitment A valid commitment hash. + function commit(bytes32 commitment) external { + require(commitmentRevealTime[commitment] < 1, "Existent commitment"); + commitmentRevealTime[commitment] = now.add(minCommitmentAge); + } + + // 2. + /// @notice Ensure the commitment is ready to be revealed. + /// @dev This method can be polled to ensure registration. + /// @param commitment Commitment to be queried. + /// @return Wether the commitment can be revealed or not. + function canReveal(bytes32 commitment) public view returns (bool) { + uint revealTime = commitmentRevealTime[commitment]; + return 0 < revealTime && revealTime <= now; + } + + ///////////////////// + // REGISTRAR ADMIN // + ///////////////////// + + /// @notice Change required commitment maturity. + /// @dev Only owner. + /// @param newMinCommitmentAge The new maturity required. + function setMinCommitmentAge (uint newMinCommitmentAge) external onlyOwner { + minCommitmentAge = newMinCommitmentAge; + } + + /// @notice Change disbaled names. + /// @dev Only owner. + /// @param newMinLength The new minimum length enabled. + function setMinLength (uint newMinLength) external onlyOwner { + minLength = newMinLength; + } +} diff --git a/package-lock.json b/package-lock.json index ba3e3e0..9740ac4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -646,6 +646,15 @@ "resolved": "https://registry.npmjs.org/@rsksmart/rns-registry/-/rns-registry-1.0.0.tgz", "integrity": "sha512-rn68uGGvU90dtdNcyXXGGhbIPsek/W7cHGN8UqYLomhAXX3OFQd+JDSVkJUBOOY92Ou9V5TYhUh3NOzSv5/1+Q==" }, + "@rsksmart/rns-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rsksmart/rns-resolver/-/rns-resolver-1.0.1.tgz", + "integrity": "sha512-Osp9ptuSyVsISHjcOiotY2NsXw3Vo27MyKNiabj7dEC5pXAx6f/pHURYIHrPoWEZOjz2Swxfs9rSN1LejhXNDA==", + "requires": { + "@openzeppelin/contracts": "^2.4.0", + "@rsksmart/rns-registry": "^1.0.0" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", diff --git a/package.json b/package.json index d3c278d..8d4eca7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@rsksmart/erc677": "^1.0.1", "@rsksmart/rns-auction-registrar": "^1.0.1", "@rsksmart/rns-registry": "^1.0.0", + "@rsksmart/rns-resolver": "^1.0.1", "@truffle/hdwallet-provider": "^1.0.25" }, "devDependencies": { diff --git a/test/fifs/fifs_addr_registrar.test.js b/test/fifs/fifs_addr_registrar.test.js new file mode 100644 index 0000000..1aa956a --- /dev/null +++ b/test/fifs/fifs_addr_registrar.test.js @@ -0,0 +1,953 @@ +const RNS = artifacts.require('RNS'); +const Token = artifacts.require('ERC677'); +const NodeOwner = artifacts.require('NodeOwner'); +const FIFSAddrRegistrar = artifacts.require('FIFSAddrRegistrar'); +const NamePrice = artifacts.require('NamePrice'); +const BytesUtils = artifacts.require('BytesUtils'); +const PublicResolver = artifacts.require('PublicResolver'); + +const namehash = require('eth-ens-namehash').hash; +const expect = require('chai').expect; +const helpers = require('@openzeppelin/test-helpers'); + +const getAddrRegisterData = require('../../utils').getAddrRegisterData; + +contract('FIFS Addr Registrar', async (accounts) => { + let rns, token, nodeOwner, fifsAddrRegistrar, namePrice, publicResolver; + const pool = accounts[6]; + const addr = accounts[8]; + + beforeEach(async () => { + const rootNode = namehash('tld'); + + rns = await RNS.new(); + token = await Token.new(accounts[0], web3.utils.toBN('1000000000000000000000'), 'RIFOS', 'RIF', web3.utils.toBN('18')); + publicResolver = await PublicResolver.new(rns.address); + + await rns.setDefaultResolver(publicResolver.address); + + nodeOwner = await NodeOwner.new( + rns.address, + rootNode, + ); + + await rns.setSubnodeOwner('0x00', web3.utils.sha3('tld'), nodeOwner.address); + + namePrice = await NamePrice.new(); + + const bytesUtils = await BytesUtils.new(); + await FIFSAddrRegistrar.link('BytesUtils', bytesUtils.address); + + fifsAddrRegistrar = await FIFSAddrRegistrar.new( + token.address, + nodeOwner.address, + pool, + namePrice.address, + rns.address, + rootNode + ); + + await nodeOwner.addRegistrar(fifsAddrRegistrar.address, { from: accounts[0] }); + }); + + it('should have deployer as owner', async () => { + const owner = await nodeOwner.owner(); + + expect(owner).to.eq(accounts[0]); + }); + + describe('committing', async () => { + const label = web3.utils.sha3('ilanolkies'); + const owner = accounts[4]; + + it('should create a commitment for a given name, owner and secret', async () => { + const commitment = await fifsAddrRegistrar.makeCommitment( + label, + owner, + '0x0000000000000000000000000000000000000000000000000000000000001234', + ); + + expect(commitment).to.not.be.null; + }); + + it('should create different commitments for different secrets', async () => { + const commitment1 = await fifsAddrRegistrar.makeCommitment( + label, + owner, + '0x0000000000000000000000000000000000000000000000000000000000001234', + ); + const commitment2 = await fifsAddrRegistrar.makeCommitment( + label, + owner, + '0x0000000000000000000000000000000000000000000000000000000000005678', + ); + expect(commitment1).to.not.equal(commitment2); + }); + }); + + describe('commitment age', async () => { + let commitment; + + beforeEach(async () => { + commitment = await fifsAddrRegistrar.makeCommitment( + web3.utils.sha3('ilanolkies'), + accounts[4], + '0x0000000000000000000000000000000000000000000000000000000000001234', + ); + }); + + it('should not be able to reveal before committing', async () => { + const canReveal = await fifsAddrRegistrar.canReveal(commitment); + + expect(canReveal).to.be.false; + }); + + it('should be able to reveal after one minute', async () => { + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + + const canReveal = await fifsAddrRegistrar.canReveal(commitment); + + expect(canReveal).to.be.true; + }); + }); + + describe('revealing', async () => { + const name = 'ilanolkies'; + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const amount = web3.utils.toBN(2); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + + let commitment; + + beforeEach(async () => { + commitment = await fifsAddrRegistrar.makeCommitment(web3.utils.sha3(name), owner, secret); + }); + + describe('should not allow to register with no commitment', async () => { + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'No commitment found' + ); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data), + 'No commitment found' + ); + }); + }); + + describe('should not allow to register before commitment maturity', async () => { + beforeEach(async () => + await fifsAddrRegistrar.commit(commitment) + ); + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'No commitment found' + ); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data), + 'No commitment found' + ); + }); + }); + + describe('should not allow to reveal with a wrong secret', async () => { + beforeEach(async () => + await fifsAddrRegistrar.commit(commitment) + ); + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + await helpers.expectRevert( + fifsAddrRegistrar.register( + name, + owner, + '0x0000000000000000000000000000000000000000000000000000000000005678', + duration, + addr + ), + 'No commitment found' + ); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData( + name, + owner, + '0x0000000000000000000000000000000000000000000000000000000000005678', + duration, + addr + ); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data), + 'No commitment found' + ); + }) + }); + + describe('should not allow to change owner of a commitment', async () => { + beforeEach(async () => + await fifsAddrRegistrar.commit(commitment) + ); + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, accounts[6], secret, duration, addr), + 'No commitment found' + ); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, accounts[6], secret, duration, addr); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data), + 'No commitment found' + ); + }); + }); + + it('should not allow to postpone a commitment', async () => { + await fifsAddrRegistrar.commit(commitment); + + await helpers.expectRevert( + fifsAddrRegistrar.commit(commitment, { from: accounts[5] }), + 'Existent commitment' + ); + }); + }); + + describe('update commitment age', async () => { + it('should not allow not owner to set min commitment age', async () => { + await helpers.expectRevert( + fifsAddrRegistrar.setMinCommitmentAge(web3.utils.toBN(1), { from: accounts[2] }), + 'Ownable: caller is not the owner' + ); + }); + + it('should allow owner to set min commitment age', async () => { + const minCommitmentAge = web3.utils.toBN(120); + + await fifsAddrRegistrar.setMinCommitmentAge(minCommitmentAge, { from: accounts[0] }); + + const actualMinCommitmentAge = await fifsAddrRegistrar.minCommitmentAge(); + + expect(actualMinCommitmentAge).to.be.bignumber.eq(minCommitmentAge); + }); + + it('should increase time for commit-reveal process', async () => { + const name = 'ilanolkies'; + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + + await token.approve(fifsAddrRegistrar.address, web3.utils.toBN(2)); + + await fifsAddrRegistrar.setMinCommitmentAge(web3.utils.toBN(120), { from: accounts[0] }); + + const commitment = await fifsAddrRegistrar.makeCommitment(web3.utils.sha3(name), owner, secret); + await fifsAddrRegistrar.commit(commitment); + + expect(await fifsAddrRegistrar.canReveal(commitment)).to.be.false; + + await helpers.time.increase(61); + + expect(await fifsAddrRegistrar.canReveal(commitment)).to.be.false; + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'No commitment found' + ); + + await helpers.time.increase(60); + + expect(await fifsAddrRegistrar.canReveal(commitment)).to.be.true; + }); + }); + + describe('initial price', async () => { + it('1 year - 2 rif', async () => { + const name = 'ilanolkies'; + const duration = web3.utils.toBN(1); + + expect( + await fifsAddrRegistrar.price(name, 0, duration) + ).to.be.bignumber.eq( + web3.utils.toBN('2000000000000000000') + ); + }); + + it('2 year - 4 rif', async () => { + const name = 'ilanolkies'; + const duration = web3.utils.toBN(2); + + expect( + await fifsAddrRegistrar.price(name, 0, duration) + ).to.be.bignumber.eq( + web3.utils.toBN('4000000000000000000') + ); + }); + + it('2+k year - k+2 rif', async () => { + const name = 'ilanolkies'; + + for (let i = 0; i < 10; i++) { + const duration = web3.utils.toBN(3).add(web3.utils.toBN(i)); + + expect( + await fifsAddrRegistrar.price(name, 0, duration) + ).to.be.bignumber.eq( + duration.add(web3.utils.toBN(2)).mul(web3.utils.toBN('1000000000000000000')) + ); + } + }); + + it('should not allow to overflow duration', async () => { + await helpers.expectRevert( + fifsAddrRegistrar.price(name, 0, helpers.constants.MAX_UINT256), + 'SafeMath: addition overflow' + ); + }); + + it('should not allow to overflow duration when multiplying', async () => { + await helpers.expectRevert( + fifsAddrRegistrar.price(name, 0, helpers.constants.MAX_UINT256.div(web3.utils.toBN('1000000000000000000'))), + 'SafeMath: multiplication overflow' + ); + }); + }); + + describe('registration', async () => { + it('should not allow to register with no token approval - approve + transfer', async () => { + const name = 'ilanolkies'; + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + + const commitment = await fifsAddrRegistrar.makeCommitment(web3.utils.sha3(name), owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'ERC20: transfer amount exceeds allowance.' + ); + }); + + describe('should require to transfer depending on duration', async () => { + const name = 'ilanolkies'; + const owner = accounts[5]; + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + + beforeEach(async () => { + const commitment = await fifsAddrRegistrar.makeCommitment(web3.utils.sha3(name), owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + }); + + describe('1 year - 2 rif', async () => { + const duration = web3.utils.toBN('1'); + const amount = web3.utils.toBN('2000000000000000000').sub(web3.utils.toBN('1')); + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'ERC20: transfer amount exceeds allowance.' + ); + }); + + it('transferAndCall', async () => { + await token.transfer(accounts[8], amount); + + const data = getAddrRegisterData(name, owner, secret, duration, addr); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data, { from: accounts[8] }), + 'Not enough tokens' + ); + }) + }); + + describe('2 years - 4 rif', async () => { + const duration = web3.utils.toBN('2'); + const amount = web3.utils.toBN('4000000000000000000').sub(web3.utils.toBN('1')); + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'ERC20: transfer amount exceeds allowance.' + ); + }); + + it('transferAndCall', async () => { + await token.transfer(accounts[8], amount); + + const data = getAddrRegisterData(name, owner, secret, duration, addr); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data, { from: accounts[8] }), + 'Not enough tokens' + ); + }) + }); + + for (let i = 0; i < 10; i++) { + describe(`${2+i} years - ${4+i} rif`, async () => { + const duration = web3.utils.toBN(3).add(web3.utils.toBN(i)); + const amount = web3.utils.toBN('4000000000000000000') + .add( + duration.sub(web3.utils.toBN(2)) + .mul(web3.utils.toBN('1000000000000000000')) + ) + .sub(web3.utils.toBN('1')); + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'ERC20: transfer amount exceeds allowance.' + ); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transfer(accounts[8], amount); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data), + 'Not enough tokens' + ); + }); + }); + } + }); + + describe('should transfer tokens to a resource pool', async () => { + const name = 'ilanolkies'; + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + let balance; + + beforeEach(async () => { + const commitment = await fifsAddrRegistrar.makeCommitment(web3.utils.sha3(name), owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + + balance = await token.balanceOf(pool); + }) + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + + afterEach(async () => { + const actualBalance = await token.balanceOf(pool); + expect(actualBalance).to.be.bignumber.eq(balance.add(amount)); + }) + }); + + describe('should only register available names', async () => { + const name = 'ilanolkies'; + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + beforeEach(async () => { + await token.approve(fifsAddrRegistrar.address, amount); + + const commitment = await fifsAddrRegistrar.makeCommitment(web3.utils.sha3(name), owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + + const commitment2 = await fifsAddrRegistrar.makeCommitment(web3.utils.sha3(name), accounts[6], secret, { from: accounts[6] }); + await fifsAddrRegistrar.commit(commitment2); + + await helpers.time.increase(61); + }); + + it('approve + transfer', async () => { + await token.transfer(accounts[6], amount); + await token.approve(fifsAddrRegistrar.address, amount, { from: accounts[6] }); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, accounts[6], secret, duration, addr, { from: accounts[6] }), + 'Not available' + ); + }); + + it('transferAndCall', async () => { + await token.transfer(accounts[6], amount); + + const data = getAddrRegisterData(name, accounts[6], secret, duration, addr); + + await helpers.expectRevert( + token.transferAndCall(fifsAddrRegistrar.address, amount, data, { from: accounts[6] }), + 'Not available' + ); + }); + }); + + describe('should register in blocks of 365 days', async () => { + let name = 'ilanolkies'; + const label = web3.utils.sha3(name); + const tokenId = web3.utils.toBN(label); + const owner = accounts[5]; + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + + let duration; + + beforeEach(async () => { + const commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + await helpers.time.increase(61); + }); + + describe('1 year', async () => { + const amount = web3.utils.toBN('2000000000000000000'); + + it('approve + transfer', async () => { + duration = web3.utils.toBN('1'); + + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + duration = web3.utils.toBN('1'); + + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + }); + + describe('2 years', async () => { + const amount = web3.utils.toBN('4000000000000000000'); + + it('approve + transfer', async () => { + duration = web3.utils.toBN('2'); + + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + duration = web3.utils.toBN('2'); + + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + }); + + for (let i = 0; i < 10; i++) { + describe(`${2+i} years`, async () => { + const amount = web3.utils.toBN('4000000000000000000').add(web3.utils.toBN(i+1).mul(web3.utils.toBN('1000000000000000000'))); + + it('approve + transfer', async () => { + duration = web3.utils.toBN(3).add(web3.utils.toBN(i)); + + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + duration = web3.utils.toBN(3).add(web3.utils.toBN(i)); + + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + }); + } + + afterEach(async () => { + const expirationTime = await nodeOwner.expirationTime(tokenId); + const now = await web3.eth.getBlock('latest').then(b => b.timestamp); + + expect(expirationTime).to.be.bignumber.eq(web3.utils.toBN(now).add(web3.utils.toBN('31536000').mul(duration))); + }); + }); + + describe('should allow to register for another owner', async () => { + const name = 'ilanolkies'; + const label = web3.utils.sha3(name); + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + beforeEach(async () => { + const commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + }); + + it('approve + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + + afterEach(async () => + expect( + await nodeOwner.ownerOf(web3.utils.toBN(label)) + ).to.eq( + accounts[5] + ) + ); + }); + + describe('should set the specified addr', async () => { + const name = 'ilanolkies'; + const label = web3.utils.sha3(name); + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + beforeEach(async () => { + const commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + }); + + it('approver + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + + afterEach(async () => + expect( + await publicResolver.addr(namehash(`${name}.tld`)) + ).to.eq(addr) + ); + }); + + describe('should remove commitment after registering', async () => { + const name = 'ilanolkies'; + const label = web3.utils.sha3(name); + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + let commitment; + + beforeEach(async () => { + commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + }); + + it('approver + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + + afterEach(async () => + expect( + await fifsAddrRegistrar.canReveal(commitment) + ).to.be.false + ); + }); + + describe('should register the name in tld owner', async () => { + const name = 'ilanolkies'; + const label = web3.utils.sha3(name); + const tokenId = web3.utils.toBN(label); + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + beforeEach(async () => { + const commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + }); + + it('approver + transfer', async () => { + await token.approve(fifsAddrRegistrar.address, amount); + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + }); + + it('transferAndCall', async () => { + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + }); + + afterEach(async () => { + const expectedExpiration = await web3.eth.getBlock('latest') + .then(b => b.timestamp) + .then(web3.utils.toBN) + .then(n => n.add(duration.mul(web3.utils.toBN('31536000')))); + + const tldOwnerEvents = await nodeOwner.getPastEvents('allEvents'); + + helpers.expectEvent.inLogs( + tldOwnerEvents, + 'ExpirationChanged', + { + tokenId, + expirationTime: expectedExpiration, + } + ); + + helpers.expectEvent.inLogs( + tldOwnerEvents, + 'Transfer', + { + from: helpers.constants.ZERO_ADDRESS, + to: fifsAddrRegistrar.address, + tokenId, + } + ); + + helpers.expectEvent.inLogs( + tldOwnerEvents, + 'Transfer', + { + from: fifsAddrRegistrar.address, + to: owner, + tokenId, + } + ); + }); + }); + + it('should not allow to duration equal to MAX_UINT256 - 1', async () => { + const duration = helpers.constants.MAX_UINT256.sub(web3.utils.toBN(1)); + + await helpers.expectRevert( + fifsAddrRegistrar.price(name, 0, duration), + 'SafeMath: addition overflow' + ); + }); + + it('should not allow to duration equal to MAX_UINT256 - 2', async () => { + const duration = helpers.constants.MAX_UINT256.sub(web3.utils.toBN(2)); + + await helpers.expectRevert( + fifsAddrRegistrar.price(name, 0, duration), + 'SafeMath: multiplication overflow' + ); + }); + + it('should not allow duration equal to 0', async () => { + await helpers.expectRevert( + fifsAddrRegistrar.price(name, 0, 0), + 'NamePrice: no zero duration' + ); + }); + }); + + describe('update name price', async () => { + it('should not allow not owner to update the namePriceContract', async () => { + const anotherNamePrice = await NamePrice.new(); + await helpers.expectRevert( + fifsAddrRegistrar.setNamePrice(anotherNamePrice.address, { from: accounts[2] }), + 'Ownable: caller is not the owner' + ); + }); + + it('should allow owner to update name price', async () => { + const anotherNamePrice = await NamePrice.new(); + + await fifsAddrRegistrar.setNamePrice(anotherNamePrice.address, { from: accounts[0] }); + + const actualNamePrice = await fifsAddrRegistrar.namePrice(); + + expect(actualNamePrice).to.be.eq(anotherNamePrice.address); + }); + + it('should emit an event when name price is updated', async () => { + const anotherNamePrice = await NamePrice.new(); + + await fifsAddrRegistrar.setNamePrice(anotherNamePrice.address, { from: accounts[0] }); + + helpers.expectEvent.inLogs( + await fifsAddrRegistrar.getPastEvents(), + 'NamePriceChanged', + { contractAddress: anotherNamePrice.address } + ); + }); + + describe('length lock', async () => { + describe('should initially lock names of length lower than 5', async () => { + let name; + + it('4 characters', () => { + name = 'ilan'; + }); + + it('3 characters', () => { + name = 'ila'; + }); + + it('2 characters', () => { + name = 'il'; + }); + + it('1 characters', () => { + name = 'i'; + }); + + it('0 characters', () => { + name = ''; + }); + + afterEach(async () => { + const label = name ? web3.utils.sha3(name) : '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + await token.approve(fifsAddrRegistrar.address, amount); + + const commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + + await helpers.expectRevert( + fifsAddrRegistrar.register(name, owner, secret, duration, addr), + 'Short names not available', + ); + }); + }); + + it('should not allow not owner to change locked names', async () => { + await helpers.expectRevert( + fifsAddrRegistrar.setMinLength(web3.utils.toBN(2), { from: accounts[5] }), + 'Ownable: caller is not the owner' + ); + + expect( + await fifsAddrRegistrar.minLength() + ).to.be.bignumber.eq( + web3.utils.toBN(5) + ); + }); + + it('should allow owner to change locked names', async () => { + const minLength = web3.utils.toBN(2); + await fifsAddrRegistrar.setMinLength(minLength, { from: accounts[0] }), + + expect( + await fifsAddrRegistrar.minLength() + ).to.be.bignumber.eq( + minLength + ); + }); + + it('should allow to register unlocked names', async () => { + await fifsAddrRegistrar.setMinLength(web3.utils.toBN(2), { from: accounts[0] }); + + const name = 'il'; + const label = web3.utils.sha3(name); + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + const amount = web3.utils.toBN('2000000000000000000'); + + await token.approve(fifsAddrRegistrar.address, amount); + + const commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + + await fifsAddrRegistrar.register(name, owner, secret, duration, addr); + + expect( + await nodeOwner.ownerOf(web3.utils.toBN(label)) + ).to.eq(owner); + }); + }); + }); + + it('should give change - transferAndCall', async () => { + const name = 'ilanolkies'; + const label = web3.utils.sha3(name); + const owner = accounts[5]; + const duration = web3.utils.toBN('1'); + const secret = '0x0000000000000000000000000000000000000000000000000000000000001234'; + + const amount = web3.utils.toBN('8000000000000000000'); + + const commitment = await fifsAddrRegistrar.makeCommitment(label, owner, secret); + await fifsAddrRegistrar.commit(commitment); + + await helpers.time.increase(61); + + const expectedBalance = await token.balanceOf(accounts[0]).then(b => b.sub(web3.utils.toBN('2000000000000000000'))); + + const data = getAddrRegisterData(name, owner, secret, duration, addr); + await token.transferAndCall(fifsAddrRegistrar.address, amount, data); + + const actualBalance = await token.balanceOf(accounts[0]); + + expect(actualBalance).to.be.bignumber.eq(expectedBalance); + }); +}); diff --git a/test/utils.test.js b/test/utils.test.js index 7b80cef..8a8e16a 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -33,4 +33,24 @@ describe('utils', () => { expect(actual).to.eq(expected); }); + + it('should parse addr register data', () => { + const name = 'ilanolkies'; + const owner = '0x0000011111222223333344444555556666677777'; + const secret = '0x1234'; + const duration = web3.utils.toBN('1'); + const addr = '0x8888899999aaaaabbbbbcccccdddddeeeeefffff'; + + const expected = + web3.utils.sha3('register(string,address,bytes32,uint,address)').slice(0, 10) + // signature 4b + '0000011111222223333344444555556666677777' + // address 20b - offest 4b + '1234000000000000000000000000000000000000000000000000000000000000' + // secret 32b - offset 24b + '0000000000000000000000000000000000000000000000000000000000000001' + // duration 32b - offset 56b + '8888899999aaaaabbbbbcccccdddddeeeeefffff' + // addr 20b - offest 88b + '696c616e6f6c6b696573'; // name - offset 108b + + const actual = utils.getAddrRegisterData(name, owner, secret, duration, addr); + + expect(actual).to.eq(expected); + }); }); diff --git a/utils/index.js b/utils/index.js index 3989769..2de905a 100644 --- a/utils/index.js +++ b/utils/index.js @@ -55,7 +55,41 @@ function getRenewData (name, duration) { return `${_signature}${_duration}${_name}`; } +/** + * registration with rif transferAndCall encoding + * @param {string} name to register + * @param {address} owner of the new name + * @param {hex} secret of the commit + * @param {BN} duration to register in years + */ +function getAddrRegisterData (name, owner, secret, duration, addr) { + // 0x + 8 bytes + const _signature = '0x5f7b99d5'; + + // 20 bytes + const _owner = owner.toLowerCase().slice(2); + + // 32 bytes + let _secret = secret.slice(2); + let padding = 64 - _secret.length; + for (let i = 0; i < padding; i++) { + _secret += '0'; + } + + // 32 bytes + _duration = numberToUint32(duration); + + // variable length + const _name = utf8ToHexString(name); + + // 20 bytes + _addr = addr.toLowerCase().slice(2); + + return `${_signature}${_owner}${_secret}${_duration}${_addr}${_name}`; +} + module.exports = { getRegisterData, getRenewData, + getAddrRegisterData, };