diff --git a/src/BuilderCodes.sol b/src/BuilderCodes.sol index 013e056..2cceac7 100644 --- a/src/BuilderCodes.sol +++ b/src/BuilderCodes.sol @@ -13,9 +13,11 @@ import {LibBit} from "solady/utils/LibBit.sol"; import {LibString} from "solady/utils/LibString.sol"; import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol"; -/// @notice Registry for builder codes +/// @title BuilderCodes /// -/// @author Coinbase +/// @notice ERC-721 NFT for builders to register codes to identify their apps +/// +/// @author Coinbase (https://github.com/base/builder-codes) contract BuilderCodes is Initializable, ERC721Upgradeable, @@ -26,14 +28,18 @@ contract BuilderCodes is IERC4906 { /// @notice EIP-712 storage structure for registry data - /// @custom:storage-location erc7201:base.flywheel.BuilderCodes + /// @custom:storage-location erc7201:base.BuilderCodes struct RegistryStorage { - /// @notice Base URI for referral code metadata + /// @dev Base URI for builder code metadata string uriPrefix; - /// @dev Mapping of referral code token IDs to payout recipients + /// @dev Mapping of builder code token IDs to payout recipients mapping(uint256 tokenId => address payoutAddress) payoutAddresses; } + //////////////////////////////////////////////////////////////// + /// Constants /// + //////////////////////////////////////////////////////////////// + /// @notice Role identifier for addresses authorized to call register or sign registrations bytes32 public constant REGISTER_ROLE = keccak256("REGISTER_ROLE"); @@ -47,31 +53,39 @@ contract BuilderCodes is bytes32 public constant REGISTRATION_TYPEHASH = keccak256("BuilderCodeRegistration(string code,address initialOwner,address payoutAddress,uint48 deadline)"); - /// @notice Allowed characters for referral codes + /// @notice Allowed characters for builder codes string public constant ALLOWED_CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyz_"; - /// @notice Allowed characters for referral codes lookup + /// @notice Allowed characters for builder codes lookup /// @dev LibString.to7BitASCIIAllowedLookup(ALLOWED_CHARACTERS) uint128 public constant ALLOWED_CHARACTERS_LOOKUP = 10633823847437083212121898993101832192; /// @notice EIP-1967 storage slot base for registry mapping using ERC-7201 - /// @dev keccak256(abi.encode(uint256(keccak256("base.flywheel.BuilderCodes")) - 1)) & ~bytes32(uint256(0xff)) + /// @dev keccak256(abi.encode(uint256(keccak256("base.BuilderCodes")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant REGISTRY_STORAGE_LOCATION = - 0xe3aaf266708e5133bd922e269bb5e8f72a7444c3b231cbf562ddc67a383e5700; + 0x015aa89e92b56dd64cffc1c9b26553e653b294bc48004bbcc753732d19b11100; - /// @notice Emitted when a referral code is registered + //////////////////////////////////////////////////////////////// + /// Events /// + //////////////////////////////////////////////////////////////// + + /// @notice Emitted when a builder code is registered /// - /// @param tokenId Token ID of the referral code - /// @param code Referral code + /// @param tokenId Token ID of the builder code + /// @param code Builder code event CodeRegistered(uint256 indexed tokenId, string code); - /// @notice Emitted when a publisher's default payout address is updated + /// @notice Emitted when a payout address is updated /// - /// @param tokenId Token ID of the referral code - /// @param payoutAddress New default payout address + /// @param tokenId Token ID of the builder code + /// @param payoutAddress New payout address event PayoutAddressUpdated(uint256 indexed tokenId, address payoutAddress); - /// @notice Emits when the contract URI is updated (ERC-7572) + //////////////////////////////////////////////////////////////// + /// Errors /// + //////////////////////////////////////////////////////////////// + + /// @notice Emitted when the contract URI is updated (ERC-7572) event ContractURIUpdated(); /// @notice Thrown when call doesn't have required permissions @@ -95,6 +109,10 @@ contract BuilderCodes is /// @notice Thrown when trying to renounce ownership (disabled for security) error OwnershipRenunciationDisabled(); + //////////////////////////////////////////////////////////////// + /// External Functions /// + //////////////////////////////////////////////////////////////// + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -104,6 +122,7 @@ contract BuilderCodes is /// /// @param initialOwner Address that will own the contract /// @param initialRegistrar Address to grant REGISTER_ROLE (can be address(0) to skip) + /// @param uriPrefix Base URI for builder code metadata function initialize(address initialOwner, address initialRegistrar, string memory uriPrefix) external initializer { if (initialOwner == address(0)) revert ZeroAddress(); @@ -117,7 +136,9 @@ contract BuilderCodes is if (initialRegistrar != address(0)) _grantRole(REGISTER_ROLE, initialRegistrar); } - /// @notice Registers a new referral code in the system with a custom value + /// @notice Registers a new builder code in the system with a custom value + /// + /// @dev Requires sender has REGISTER_ROLE /// /// @param code Custom builder code for the builder code /// @param initialOwner Owner of the builder code @@ -129,33 +150,36 @@ contract BuilderCodes is _register(code, initialOwner, initialPayoutAddress); } - /// @notice Registers a new referral code in the system with a signature + /// @notice Registers a new builder code in the system with a signature + /// + /// @dev Requires registration deadline has not passed + /// @dev Requires valid signature from an address with REGISTER_ROLE /// /// @param code Custom builder code for the builder code /// @param initialOwner Owner of the builder code /// @param initialPayoutAddress Default payout address /// @param deadline Deadline to submit the registration - /// @param registrar Address of the registrar - /// @param signature Signature of the registrar + /// @param signer Address of the signer + /// @param signature Registration signature function registerWithSignature( string memory code, address initialOwner, address initialPayoutAddress, uint48 deadline, - address registrar, + address signer, bytes memory signature ) external { // Check deadline has not passed if (block.timestamp > deadline) revert AfterRegistrationDeadline(deadline); - // Check registrar has role - _checkRole(REGISTER_ROLE, registrar); + // Check signer has role + _checkRole(REGISTER_ROLE, signer); // Check signature is valid bytes32 structHash = keccak256( abi.encode(REGISTRATION_TYPEHASH, keccak256(bytes(code)), initialOwner, initialPayoutAddress, deadline) ); - if (!SignatureCheckerLib.isValidSignatureNow(registrar, _hashTypedData(structHash), signature)) { + if (!SignatureCheckerLib.isValidSignatureNow(signer, _hashTypedData(structHash), signature)) { revert Unauthorized(); } @@ -168,12 +192,14 @@ contract BuilderCodes is /// @dev ERC721Upgradeable.safeTransferFrom inherits this function (and no other functions can initiate transfers) function transferFrom(address from, address to, uint256 tokenId) public override(ERC721Upgradeable, IERC721) { _checkRole(TRANSFER_ROLE, msg.sender); - // test super.transferFrom(from, to, tokenId); } /// @notice Updates the metadata for a builder code /// + /// @dev Requires sender has METADATA_ROLE + /// @dev Requires token exists + /// /// @param tokenId Token ID of the builder code function updateMetadata(uint256 tokenId) external onlyRole(METADATA_ROLE) { _requireOwned(tokenId); // verifies token exists @@ -182,6 +208,8 @@ contract BuilderCodes is /// @notice Updates the base URI for the builder codes /// + /// @dev Requires sender has METADATA_ROLE + /// /// @param uriPrefix New base URI for the builder codes function updateBaseURI(string memory uriPrefix) external onlyRole(METADATA_ROLE) { _getRegistryStorage().uriPrefix = uriPrefix; @@ -189,83 +217,90 @@ contract BuilderCodes is emit ContractURIUpdated(); } - /// @notice Updates the default payout address for a referral code + /// @notice Updates the payout address for a builder code + /// + /// @dev Requires sender is owner of builder code /// /// @param code Builder code - /// @param newPayoutAddress New default payout address - /// @dev Only callable by referral code owner + /// @param newPayoutAddress New payout address function updatePayoutAddress(string memory code, address newPayoutAddress) external { uint256 tokenId = toTokenId(code); if (_requireOwned(tokenId) != msg.sender) revert Unauthorized(); _updatePayoutAddress(tokenId, newPayoutAddress); } - /// @notice Gets the default payout address for a referral code + /// @notice Gets the payout address for a builder code + /// + /// @dev Requires builder code exists /// /// @param code Builder code /// - /// @return The default payout address + /// @return payoutAddress The payout address function payoutAddress(string memory code) external view returns (address) { uint256 tokenId = toTokenId(code); if (_ownerOf(tokenId) == address(0)) revert Unregistered(code); return _getRegistryStorage().payoutAddresses[tokenId]; } - /// @notice Gets the default payout address for a referral code + /// @notice Gets the payout address for a builder code /// - /// @param tokenId Token ID of the referral code + /// @dev Requires builder code exists + /// + /// @param tokenId Token ID of the builder code /// - /// @return The default payout address + /// @return payoutAddress The payout address function payoutAddress(uint256 tokenId) external view returns (address) { if (_ownerOf(tokenId) == address(0)) revert Unregistered(toCode(tokenId)); return _getRegistryStorage().payoutAddresses[tokenId]; } - /// @notice Returns the URI for a referral code + /// @notice Returns the URI for a builder code /// /// @param code Builder code /// - /// @return The URI for the referral code + /// @return uri The URI for the builder code function codeURI(string memory code) external view returns (string memory) { return tokenURI(toTokenId(code)); } /// @notice Returns the URI for the contract /// - /// @return The URI for the contract + /// @return uri The URI for the contract function contractURI() external view returns (string memory) { string memory uriPrefix = _getRegistryStorage().uriPrefix; return bytes(uriPrefix).length > 0 ? string.concat(uriPrefix, "contractURI.json") : ""; } - /// @notice Returns the URI for a referral code + /// @notice Returns the URI for a builder code /// - /// @param tokenId Token ID of the referral code + /// @param tokenId Token ID of the builder code /// - /// @return uri The URI for the referral code + /// @return uri The URI for the builder code function tokenURI(uint256 tokenId) public view override returns (string memory) { _requireOwned(tokenId); // verifies token exists string memory uriPrefix = _getRegistryStorage().uriPrefix; return bytes(uriPrefix).length > 0 ? string.concat(uriPrefix, toCode(tokenId)) : ""; } - /// @notice Checks if a referral code exists + /// @notice Checks if a builder code exists /// /// @param code Builder code to check /// - /// @return True if the referral code exists + /// @return registered True if the builder code exists function isRegistered(string memory code) public view returns (bool) { return _ownerOf(toTokenId(code)) != address(0); } /// @notice Checks if an address has a role /// + /// @dev Override grants all roles to owner + /// /// @param role The role to check /// @param account The address to check /// - /// @return True if the address has the role + /// @return hasRole True if the address has the role function hasRole(bytes32 role, address account) public view override returns (bool) { - return account == owner() || super.hasRole(role, account); + return super.hasRole(role, account) || account == owner(); } /// @inheritdoc ERC721Upgradeable @@ -279,11 +314,11 @@ contract BuilderCodes is || AccessControlUpgradeable.supportsInterface(interfaceId) || interfaceId == bytes4(0x49064906); } - /// @notice Checks if a referral code is valid + /// @notice Checks if a builder code is valid /// /// @param code Builder code to check /// - /// @return True if the referral code is valid + /// @return valid True if the builder code is valid function isValidCode(string memory code) public pure returns (bool) { // Early return invalid if code is zero or over 32 bytes/characters uint256 length = bytes(code).length; @@ -293,11 +328,13 @@ contract BuilderCodes is return LibString.is7BitASCII(code, ALLOWED_CHARACTERS_LOOKUP); } - /// @notice Converts a referral code to a token ID + /// @notice Converts a builder code to a token ID + /// + /// @dev Requires builder code is valid /// /// @param code Builder code to convert /// - /// @return tokenId The token ID for the referral code + /// @return tokenId The token ID for the builder code function toTokenId(string memory code) public pure returns (uint256 tokenId) { if (!isValidCode(code)) revert InvalidCode(code); @@ -306,11 +343,13 @@ contract BuilderCodes is tokenId = uint256(bytes32(bytes(code))) >> trailingZeroBytes * 8; } - /// @notice Converts a token ID to a referral code + /// @notice Converts a token ID to a builder code + /// + /// @dev Requires token ID and builder code are valid /// /// @param tokenId Token ID to convert /// - /// @return code The referral code for the token ID + /// @return code The builder code for the token ID function toCode(uint256 tokenId) public pure returns (string memory code) { // Shift nonzero bytes left so low-endian bits are zero, matching LibString's expectation to trim `\0` bytes uint256 leadingZeroBytes = LibBit.clz(tokenId) / 8; // "clz" = count leading zeros @@ -327,9 +366,13 @@ contract BuilderCodes is revert OwnershipRenunciationDisabled(); } - /// @notice Registers a new referral code + //////////////////////////////////////////////////////////////// + /// Internal Functions /// + //////////////////////////////////////////////////////////////// + + /// @notice Registers a new builder code /// - /// @param code Referral code + /// @param code Builder code /// @param initialOwner Owner of the ref code /// @param initialPayoutAddress Default payout address function _register(string memory code, address initialOwner, address initialPayoutAddress) internal { @@ -339,9 +382,11 @@ contract BuilderCodes is _updatePayoutAddress(tokenId, initialPayoutAddress); } - /// @notice Registers a new referral code + /// @notice Registers a new builder code /// - /// @param tokenId Token ID of the referral code + /// @dev Requires non-zero payout address + /// + /// @param tokenId Token ID of the builder code /// @param newPayoutAddress New payout address function _updatePayoutAddress(uint256 tokenId, address newPayoutAddress) internal { if (newPayoutAddress == address(0)) revert ZeroAddress(); @@ -351,13 +396,15 @@ contract BuilderCodes is /// @notice Authorization for upgrades /// + /// @dev Requires sender is owner + /// /// @param newImplementation Address of new implementation function _authorizeUpgrade(address newImplementation) internal view override onlyOwner {} - /// @notice Returns the domain name and version for the referral codes + /// @notice Returns the domain name and version for the builder codes /// - /// @return name The domain name for the referral codes - /// @return version The version of the referral codes + /// @return name The domain name for the builder codes + /// @return version The version of the builder codes function _domainNameAndVersion() internal pure @@ -371,7 +418,7 @@ contract BuilderCodes is /// @notice Returns if the domain name and version may change /// - /// @return True if the domain name and version may change + /// @return res True if the domain name and version may change function _domainNameAndVersionMayChange() internal pure override returns (bool) { return true; }