Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion script/deploy/DeployBaseRegistrar.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ contract DeployBaseRegistrar is Script {
address ensAddress = vm.envAddress("REGISTRY_ADDR"); // deployer-owned registry
(, bytes32 node) = NameEncoder.dnsEncodeName("basetest.eth");

BaseRegistrar base = new BaseRegistrar(ENS(ensAddress), deployerAddress, node);
BaseRegistrar base = new BaseRegistrar(ENS(ensAddress), deployerAddress, node, "", "");
Comment thread
cb-elileers marked this conversation as resolved.

console.log("Base Registrar deployed to:");
console.log(address(base));
Expand Down
80 changes: 76 additions & 4 deletions src/L2/BaseRegistrar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ERC721} from "lib/solady/src/tokens/ERC721.sol";
import {IERC721} from "openzeppelin-contracts/contracts/token/ERC721/IERC721.sol";
import {IERC165} from "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol";
import {Ownable} from "solady/auth/Ownable.sol";
import {LibString} from "solady/utils/LibString.sol";

import {GRACE_PERIOD} from "src/util/Constants.sol";

Expand All @@ -21,6 +22,8 @@ import {GRACE_PERIOD} from "src/util/Constants.sol";
///
/// @author Coinbase (https://github.com/base-org/usernames)
contract BaseRegistrar is ERC721, Ownable {
using LibString for uint256;
Comment thread
cb-elileers marked this conversation as resolved.

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* STORAGE */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand All @@ -34,6 +37,12 @@ contract BaseRegistrar is ERC721, Ownable {
/// @notice The namehash of the TLD this registrar owns (eg, base.eth).
bytes32 public immutable baseNode;

/// @notice The base URI for token metadata.
string private _baseURI;

/// @notice The URI for collection metadata.
string private _collectionURI;

/// @notice A map of addresses that are authorised to register and renew names.
mapping(address controller => bool isApproved) public controllers;

Expand Down Expand Up @@ -64,6 +73,11 @@ contract BaseRegistrar is ERC721, Ownable {
/// @param tokenId The id of the name that is not available.
error NotAvailable(uint256 tokenId);

/// @notice Thrown when the queried tokenId does not exist.
///
/// @param tokenId The id of the name that does not exist.
error NonexistentToken(uint256 tokenId);

/// @notice Thrown when the name is not registered or in its Grace Period.
///
/// @param tokenId The id of the token that is not registered or in Grace Period.
Expand Down Expand Up @@ -113,6 +127,22 @@ contract BaseRegistrar is ERC721, Ownable {
uint256 indexed id, address indexed owner, uint256 expires, address resolver, uint64 ttl
);

/// @notice Emitted when metadata for a token range is updated.
///
/// @dev Useful for third-party platforms such as NFT marketplaces who can update
/// the images and related attributes of the NFTs in a timely fashion.
/// To refresh a whole collection, emit `_toTokenId` with `type(uint256).max`
/// ERC-4906: https://eip.tools/eip/4906
///
/// @param _fromTokenId The starting range of `tokenId` for which metadata has been updated.
/// @param _toTokenId The ending range of `tokenId` for which metadata has been updated.
event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId);

/// @notice Emitted when the metadata for the contract collection is updated.
///
/// @dev ERC-7572: https://eips.ethereum.org/EIPS/eip-7572
event ContractURIUpdated();

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
/* MODIFIERS */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -154,10 +184,20 @@ contract BaseRegistrar is ERC721, Ownable {
/// @param registry_ The Registry contract.
/// @param owner_ The permissioned address initialized as the `owner` in the `Ownable` context.
/// @param baseNode_ The node that this contract manages registrations for.
constructor(ENS registry_, address owner_, bytes32 baseNode_) {
/// @param baseURI_ The base token URI for NFT metadata.
/// @param collectionURI_ The URI for the collection's metadata.
constructor(
ENS registry_,
address owner_,
bytes32 baseNode_,
string memory baseURI_,
string memory collectionURI_
) {
_initializeOwner(owner_);
registry = registry_;
baseNode = baseNode_;
_baseURI = baseURI_;
_collectionURI = collectionURI_;
}

/// @notice Authorises a controller, who can register and renew domains.
Expand Down Expand Up @@ -316,9 +356,41 @@ contract BaseRegistrar is ERC721, Ownable {
return "BASENAME";
}

/// @dev Returns the Uniform Resource Identifier (URI) for token `id`.
function tokenURI(uint256) public pure override returns (string memory) {
return "";
/// @notice Returns the Uniform Resource Identifier (URI) for token `id`.
///
/// @dev Reverts if the `tokenId` has not be registered.
///
/// @param tokenId The token for which to return the metadata uri.
///
/// @return The URI for the specified `tokenId`.
function tokenURI(uint256 tokenId) public view override returns (string memory) {
if (_ownerOf(tokenId) == address(0)) revert NonexistentToken(tokenId);

return bytes(_baseURI).length > 0 ? string.concat(_baseURI, tokenId.toString()) : "";
}

/// @notice Returns the Uniform Resource Identifier (URI) for the contract.
///
/// @dev ERC-7572: https://eips.ethereum.org/EIPS/eip-7572
function contractURI() public view returns (string memory) {
return _collectionURI;
}

/// @dev Allows the owner to set the the base Uniform Resource Identifier (URI)`.
/// Emits the `BatchMetadataUpdate` event for the full range of valid `tokenIds`.
function setBaseTokenURI(string memory baseURI_) public onlyOwner {
_baseURI = baseURI_;
/// @dev minimum valid tokenId is `1` because uint256(nodehash) will never be called against `nodehash == 0x0`.
uint256 minTokenId = 1;
uint256 maxTokenId = type(uint256).max;
emit BatchMetadataUpdate(minTokenId, maxTokenId);
}

/// @dev Allows the owner to set the the contract Uniform Resource Identifier (URI)`.
/// Emits the `ContractURIUpdated` event.
function setContractURI(string memory collectionURI_) public onlyOwner {
_collectionURI = collectionURI_;
emit ContractURIUpdated();
}

/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
Expand Down
4 changes: 3 additions & 1 deletion test/BaseRegistrar/BaseRegistrarBase.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ contract BaseRegistrarBase is Test {
bytes32 public node = keccak256(abi.encodePacked(BASE_ETH_NODE, label));
uint256 public duration = 365 days;
uint256 public blockTimestamp = 1716496498; // May 23, 2024
string public baseURI = "https://base.org/api/basenames/metadata/";
string public collectionURI = "https://base.org/api/basenames/contract/";

function setUp() public {
vm.prank(owner);
registry = new Registry(owner);
baseRegistrar = new BaseRegistrar(registry, owner, BASE_ETH_NODE);
baseRegistrar = new BaseRegistrar(registry, owner, BASE_ETH_NODE, baseURI, collectionURI);
_ensSetup();
}

Expand Down
11 changes: 11 additions & 0 deletions test/BaseRegistrar/ContractURI.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Test} from "forge-std/Test.sol";
import {BaseRegistrarBase} from "./BaseRegistrarBase.t.sol";

contract ContractURI is BaseRegistrarBase {
function test_contractURI_isReturnedAsExpected() public view {
assertEq(keccak256(bytes(baseRegistrar.contractURI())), keccak256(bytes(collectionURI)));
}
}
36 changes: 36 additions & 0 deletions test/BaseRegistrar/SetBaseTokenURI.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {BaseRegistrar} from "src/L2/BaseRegistrar.sol";
import {BaseRegistrarBase} from "./BaseRegistrarBase.t.sol";
import {Ownable} from "solady/auth/Ownable.sol";
import {LibString} from "solady/utils/LibString.sol";

contract SetBaseTokenURI is BaseRegistrarBase {
using LibString for uint256;

string public newBaseURI = "https://newurl.org/";

function test_allowsTheOwnerToSetTheBaseURI() public {
vm.expectEmit(address(baseRegistrar));
emit BaseRegistrar.BatchMetadataUpdate(1, type(uint256).max);
vm.prank(owner);
baseRegistrar.setBaseTokenURI(newBaseURI);

_registrationSetup();
vm.warp(blockTimestamp);
vm.prank(controller);
baseRegistrar.register(id, user, duration);

string memory returnedURI = baseRegistrar.tokenURI(id);
string memory expectedURI = string.concat(newBaseURI, id.toString());
assertEq(keccak256(bytes(returnedURI)), keccak256(bytes(expectedURI)));
}

function test_reverts_whenCalledByNonOwner(address caller) public {
vm.assume(caller != owner);
vm.prank(caller);
vm.expectRevert(Ownable.Unauthorized.selector);
baseRegistrar.setBaseTokenURI(newBaseURI);
}
}
27 changes: 27 additions & 0 deletions test/BaseRegistrar/SetContractURI.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import {Test} from "forge-std/Test.sol";
import {BaseRegistrarBase} from "./BaseRegistrarBase.t.sol";
import {BaseRegistrar} from "src/L2/BaseRegistrar.sol";
import {Ownable} from "solady/auth/Ownable.sol";

contract SetContractURI is BaseRegistrarBase {
string newContractURI = "NewURI";

function test_allowsTheOwnerToSetTheContractURI() public {
vm.expectEmit(address(baseRegistrar));
emit BaseRegistrar.ContractURIUpdated();

vm.prank(owner);
baseRegistrar.setContractURI(newContractURI);
assertEq(keccak256(bytes(baseRegistrar.contractURI())), keccak256(bytes(newContractURI)));
}

function test_reverts_whenCalledByNonOwner(address caller) public {
vm.assume(caller != owner);
vm.prank(caller);
vm.expectRevert(Ownable.Unauthorized.selector);
baseRegistrar.setBaseTokenURI(newContractURI);
}
}
2 changes: 1 addition & 1 deletion test/BaseRegistrar/Symbol.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {Test} from "forge-std/Test.sol";
import {BaseRegistrarBase} from "./BaseRegistrarBase.t.sol";

contract Symbol is BaseRegistrarBase {
function test_nameIsSetAsExpected() public view {
function test_symbolIsSetAsExpected() public view {
string memory expectedSymbol = "BASENAME";
assertEq(keccak256(bytes(baseRegistrar.symbol())), keccak256(bytes(expectedSymbol)));
}
Expand Down
32 changes: 29 additions & 3 deletions test/BaseRegistrar/TokenURI.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,36 @@ pragma solidity ^0.8.23;

import {Test} from "forge-std/Test.sol";
import {BaseRegistrarBase} from "./BaseRegistrarBase.t.sol";
import {BaseRegistrar} from "src/L2/BaseRegistrar.sol";
import {LibString} from "solady/utils/LibString.sol";

contract TokenURI is BaseRegistrarBase {
function test_nameIsSetAsExpected() public view {
string memory expectedURI = "";
assertEq(keccak256(bytes(baseRegistrar.tokenURI(1))), keccak256(bytes(expectedURI)));
using LibString for uint256;

function test_tokenURIIsSetAsExpected() public {
_registrationSetup();
vm.warp(blockTimestamp);
vm.prank(controller);
baseRegistrar.register(id, user, duration);

string memory expectedURI = string.concat(baseURI, id.toString());
assertEq(keccak256(bytes(baseRegistrar.tokenURI(id))), keccak256(bytes(expectedURI)));
}

function test_returnsTokenURI_ifTheTokenIsExpired() public {
_registrationSetup();
vm.warp(blockTimestamp);
vm.prank(controller);
uint256 expires = baseRegistrar.register(id, user, duration);
vm.warp(expires + 1);
baseRegistrar.tokenURI(id);

string memory expectedURI = string.concat(baseURI, id.toString());
assertEq(keccak256(bytes(baseRegistrar.tokenURI(id))), keccak256(bytes(expectedURI)));
}

function test_reverts_ifTheTokenHasNotBeenRegistered() public {
vm.expectRevert(abi.encodeWithSelector(BaseRegistrar.NonexistentToken.selector, id));
baseRegistrar.tokenURI(id);
}
}
2 changes: 1 addition & 1 deletion test/IntegrationTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ contract IntegrationTest is Test {
rentPrices[5] = 3_170_979; //3,170,979.1983764587 = 1e14 / (365 * 24 * 3600)

exponentialPremiumPriceOracle = new ExponentialPremiumPriceOracle(rentPrices, 1e18, 21);
baseRegistrar = new BaseRegistrar(registry, owner, BASE_ETH_NODE);
baseRegistrar = new BaseRegistrar(registry, owner, BASE_ETH_NODE, "", "");

_establishNamespaces();

Expand Down