diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09880b1..f8a378b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: test -on: workflow_dispatch +on: push env: FOUNDRY_PROFILE: ci @@ -29,6 +29,5 @@ jobs: id: build - name: Run Forge tests - run: | - forge test -vvv + run: forge test -vvvv --summary --detailed --gas-report id: test diff --git a/README.md b/README.md index c941148..599aeca 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,11 @@ Tiny web3 protocols and applications. The aim is to keep it simple, quick to implement and interesting to learn Solidity. Inspired by [lil-web3](https://github.com/m1guelpf/lil-web3) by [Miguel Piedrafita](https://twitter.com/m1guelpf). + +## Table of contents + +- [Tiny ENS](#tiny-ens) + +## Tiny ENS + +Map human-readable names like 'vitalik.eth' to machine-readable identifiers such as Ethereum addresses like '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' and support the reverse resolution. diff --git a/foundry.toml b/foundry.toml index 25b918f..92cd217 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,5 +2,8 @@ src = "src" out = "out" libs = ["lib"] +remappings = [ + '@forge-std/=lib/forge-std/src/', +] # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/src/TinyENS.sol b/src/TinyENS.sol new file mode 100644 index 0000000..d4cb583 --- /dev/null +++ b/src/TinyENS.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +/// @notice Interface for TinyENS. +interface ITinyENS { + /// @notice Register a new ENS name and link it to an address. + /// @param name The ENS name to register. + function register(string memory name) external; + + /// @notice Update the ENS name linked to an address. + /// @param newName The new ENS name. + function update(string memory newName) external; + + /// @notice Resolve the address associated with a given ENS name. + /// @param name The ENS name to resolve. + /// @return The address associated with the ENS name. + function resolve(string memory name) external view returns (address); + + /// @notice Reverse resolve an address to its associated ENS name. + /// @param targetAddress The target address to reverse resolve. + /// @return The ENS name associated with the address. + function reverse(address targetAddress) external view returns (string memory); +} + +/// @title Tiny Ethereum Name Service +/// @author leovct +/// @notice Map human-readable names like 'vitalik.eth' to machine-readable identifiers such as +/// Ethereum addresses like '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' and support the reverse +/// resolution. +contract TinyENS is ITinyENS { + /// @notice Thrown when trying to register a name already registered. + error AlreadyRegistered(); + + /// @notice Map ENS names to addresses. + mapping(string => address) private registry; + + /// @notice Map addresses to ENS names. + mapping(address => string) private reverseRegistry; + + modifier notRegistered(string memory name) { + if (registry[name] != address(0)) revert AlreadyRegistered(); + _; + } + + function register(string memory name) public notRegistered(name) { + registry[name] = msg.sender; + reverseRegistry[msg.sender] = name; + } + + function update(string memory newName) external notRegistered(newName) { + // Unlink the old name and the address. + string memory oldName = reverseRegistry[msg.sender]; + registry[oldName] = address(0); + // Register the new name. + register(newName); + } + + function resolve(string memory name) external view returns (address) { + return registry[name]; + } + + function reverse(address targetAddress) external view returns (string memory) { + return reverseRegistry[targetAddress]; + } +} diff --git a/test/TinyENS.t.sol b/test/TinyENS.t.sol new file mode 100644 index 0000000..5611622 --- /dev/null +++ b/test/TinyENS.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import '../src/TinyENS.sol'; +import '@forge-std/Test.sol'; + +contract TinyENSTest is Test { + TinyENS private tinyENS; + + address private owner = makeAddr('owner'); + address private alice = makeAddr('alice'); + address private bob = makeAddr('bob'); + + function setUp() public { + vm.startPrank(owner); + tinyENS = new TinyENS(); + console2.log('TinyENS deployed'); + vm.stopPrank(); + } + + function testRegisterNewName() public { + vm.startPrank(alice); + tinyENS.register('alice.eth'); + vm.stopPrank(); + + assertEq(tinyENS.resolve('alice.eth'), alice); + assertEq(tinyENS.reverse(alice), 'alice.eth'); + console2.log('Alice registed alice.eth'); + } + + function testRegisterAlreadyRegisteredName() public { + vm.startPrank(alice); + tinyENS.register('alice.eth'); + assertEq(tinyENS.resolve('alice.eth'), alice); + assertEq(tinyENS.reverse(alice), 'alice.eth'); + console2.log('Alice registed alice.eth'); + + console2.log('Bob tries to register alice.eth'); + vm.startPrank(bob); + vm.expectRevert(TinyENS.AlreadyRegistered.selector); + tinyENS.register('alice.eth'); + assertEq(tinyENS.resolve('alice.eth'), alice); + assertEq(tinyENS.reverse(alice), 'alice.eth'); + assertEq(tinyENS.reverse(bob), ''); + vm.stopPrank(); + } + + function testUpdateWithNewName() public { + vm.startPrank(alice); + tinyENS.register('alice.eth'); + assertEq(tinyENS.resolve('alice.eth'), alice); + assertEq(tinyENS.reverse(alice), 'alice.eth'); + console2.log('Alice registed alice.eth'); + + tinyENS.register('alice22.eth'); + assertEq(tinyENS.resolve('alice22.eth'), alice); + assertEq(tinyENS.reverse(alice), 'alice22.eth'); + console2.log('Alice updated its name to alice22.eth'); + vm.stopPrank(); + } + + function testUpdateWithAlreadyRegisteredName() public { + vm.startPrank(alice); + tinyENS.register('alice.eth'); + console2.log('Alice registed alice.eth'); + + vm.startPrank(bob); + tinyENS.register('bob.eth'); + console2.log('Bob registed bob.eth'); + + console2.log('Bob tries to update its name to alice.eth'); + vm.expectRevert(TinyENS.AlreadyRegistered.selector); + tinyENS.update('alice.eth'); + assertEq(tinyENS.resolve('alice.eth'), alice); + assertEq(tinyENS.reverse(alice), 'alice.eth'); + assertEq(tinyENS.resolve('bob.eth'), bob); + assertEq(tinyENS.reverse(bob), 'bob.eth'); + vm.stopPrank(); + } +}