Users(addresses) can claim a name, which is recorded on-chain.
Each name can only have one owner. (1-to-1 mapping)
A user can own multiple names (1-to-many mapping)
User can relinquish their ownership of a name -> therefore, available for acquisition.
Base Code
SimpleNameRegister.sol
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.13;/**@title An on-chain name registry@author Calnix@notice A registed name can be released, availing to be registered by another user*/contract SimpleNameRegister {/// @notice Map a name to an address to identify current holder mapping (string=>address) public holder; /// @notice Emit event when a name is registeredeventRegister(addressindexed holder, string name);/// @notice Emit event when a name is releasedeventRelease(addressindexed holder, string name);/// @notice User can register an available name/// @param name The string to registerfunctionregister(stringcalldata name) external {require(holder[name] ==address(0),"Already registered!"); holder[name] = msg.sender;emitRegister(msg.sender, name); }/// @notice Holder can release a name, making it available/// @param name The string to releasefunctionrelease(stringcalldata name) external {require(holder[name] == msg.sender,"Not your name!");delete holder[name];emitRelease(msg.sender, name); }}
1. Mapping associating names to their respective owner's address
mapping (string=>address) public holder;
initialized as a public state variable.
solidity will automatically create a getter function for it, which we can use to pass a name as parameter to check ownership.
If there is no owner, the address returned will be 0x0000000000000000000000000000000000000000
2. Function to allow a user to register their ownership of a name
require statement checks if the name passed is indeed registered to the function caller (msg.sender)
If so, we reset the mapped address of the name to the zero address.
mappings can be seen as hash tables which are virtually initialized such that every possible key exists and is mapped to default values.
the default value for address type will be 0x0000000000000000000000000000000000000000 or address(0)
event Releaseemitted each time a name is relinquished.
Events
Events allow us to “print” information on the blockchain in a way that is more searchable and gas efficient than just saving to public storage variables in our smart contracts.
A single address could be the owner of multiple names.
It would be useful if the end-user could simply supply their address to check which are the names registered to them.
Achieving this one to many structure on-chain would not be gas efficient.
Solution: Using Events
Run a background task that subscribes to events from the contract.
This background task will listen to events as they are emitted and track the list of addresses for each owner via some centralized database.
Website front-end can read from this database and reflect accordingly.
Testing
Testing in Solidity is somewhat different from other frameworks like brownie, as they would be done in Solidity.
If the test function reverts, the test fails, otherwise it passes.
Essentially we deploy a test contract (0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84) containing our test functions.
// SPDX-License-Identifier: UNLICENSEDpragmasolidity ^0.8.13;import"ds-test/test.sol";import'src/SimpleNameRegister.sol';interface CheatCodes {functionprank(address) external;functionexpectRevert(bytescalldata) external;functionstartPrank(address) external;functionstopPrank() external;}contractSimpleNameRegisterTestisDSTest {// declare state var. SimpleNameRegister simpleNameRegister; CheatCodes cheats;address adversary;functionsetUp() public { simpleNameRegister =newSimpleNameRegister(); cheats =CheatCodes(HEVM_ADDRESS); adversary =0xE6A2e85916802210147e366D4431f5ca4dD51a78; }// user can register an available namefunctiontestRegisterName(stringmemory_testString) public { simpleNameRegister.registerName(_testString);bool success = (address(this) == simpleNameRegister.nameOwner(_testString));assertTrue(success); }// user can register an available name and relinquish itfunctiontestRelinquishName(stringmemory_testString) public { simpleNameRegister.registerName(_testString); simpleNameRegister.relinquishName(_testString);bool success = (simpleNameRegister.nameOwner(_testString) ==address(0));assertTrue(success); }// user cannot relinquish a name that does not belong to themfunctiontestRelinquishAsNotOwner(stringmemory_testString) public { simpleNameRegister.registerName(_testString); cheats.startPrank(adversary); cheats.expectRevert(bytes("The provided name does not belong to you!")); simpleNameRegister.relinquishName(_testString); cheats.stopPrank(); }// user cannot register a name that already has an ownerfunctiontestRegisterUnavailableName(stringmemory_testString) public { simpleNameRegister.registerName(_testString); cheats.startPrank(adversary); cheats.expectRevert(bytes("The provided name has already been registered!")); simpleNameRegister.registerName(_testString); cheats.stopPrank(); }}
pragmasolidity ^0.8.13;import"ds-test/test.sol";import'src/SimpleNameRegister.sol';import"forge-std/console2.sol";interface CheatCodes {functionprank(address) external;functionexpectRevert(bytescalldata) external;functionlabel(address addr,stringcalldata label) external;}abstractcontractStateZeroisDSTest { SimpleNameRegister public simpleNameRegister; CheatCodes cheats;address user;functionsetUp() publicvirtual { simpleNameRegister =newSimpleNameRegister(); cheats =CheatCodes(HEVM_ADDRESS); user =0x0000000000000000000000000000000000000001; cheats.label(user,"user"); }}contractStateZeroTestisStateZero {functiontestCannotRelease(stringmemory testString) public { console2.log("Cannot release a name that you do not hold"); cheats.prank(user); cheats.expectRevert(bytes("Not your name!")); simpleNameRegister.release(testString); }functiontestRegister(stringmemory testString) public { console2.log("User registers a name"); cheats.prank(user); simpleNameRegister.register(testString);bool success = (user == simpleNameRegister.holder(testString));assertTrue(success); }}abstractcontractStateRegisteredisStateZero {address adversary;string name;functionsetUp() publicoverride { super.setUp(); adversary =0xE6A2e85916802210147e366D4431f5ca4dD51a78; cheats.label(adversary,"adversary");// state transition name ='whale'; cheats.prank(user); simpleNameRegister.register(name); }}contractStateRegisteredTestisStateRegistered {functiontestAdversaryCannotRegisterName() public { console2.log("Adversary cannot register name belonging to User"); cheats.prank(adversary); cheats.expectRevert(bytes("Already registered!")); simpleNameRegister.register(name); }functiontestAdversaryCannotReleaseName() public { console2.log("Adversary cannot release name belonging to User"); cheats.prank(adversary); cheats.expectRevert(bytes("Not your name!")); simpleNameRegister.release(name); }functiontestUserCannotRegisterOwnedName() public { console2.log("User cannot register a name that he already holds"); cheats.prank(user); cheats.expectRevert(bytes("Already registered!")); simpleNameRegister.register(name); }functiontestUserRelease() public { console2.log("User releases name that he holds"); cheats.prank(user); simpleNameRegister.release(name);bool success = (address(0) == simpleNameRegister.holder(name));assertTrue(success); }}
function setUp()
The setup function is invoked before each test case is run. Serves to setup the necessary variables and conditions for your test functions to operate.
deploy a fresh instance of SimpleNameRegister before each test function is ran via simpleNameRegister = new SimpleNameRegister()
Running the above three commands manually, each time can be bothersome. To that end, create a new file 'MakeFile' in the project root directory if one does not exist.
# include .env file and export its env vars (-include to ignore error if it does not exist)include.envdeploy:forgecreatesrc/SimpleNameRegister.sol:SimpleNameRegister--private-key ${PRIVATE_KEY_EDGE} --rpc-url ${ETH_RPC_URL}verify:forgeverify-contract--chain-id ${KOVAN_CHAINID} --compiler-versionv0.8.13+commit.abaa5c0e ${CONTRACT_ADDRESS} src/SimpleNameRegister.sol:SimpleNameRegister ${ETHERSCAN_API_KEY} --num-of-optimizations200--flattenverify-check:forgeverify-check--chain-id ${KOVAN_CHAINID} ${GUID} ${ETHERSCAN_API_KEY}
Now you will only need to run:
make deploy
make verify
make verify-check
Be sure to update the contract address and GUID values in .env for each deployment as they would change.
Note:
For make verify, the --flatten flag may ONLY be necessary on a window machine. If verification fails, try without it.
If it still fails, its in god's hands now.