Skip to content
Merged
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
161 changes: 104 additions & 57 deletions src/BuilderCodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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");

Expand All @@ -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;
Comment on lines -60 to +66
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-zero source change


/// @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
Expand All @@ -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();
Expand All @@ -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();

Expand All @@ -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
Expand All @@ -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();
}

Expand All @@ -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
Expand All @@ -182,90 +208,99 @@ 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;
emit BatchMetadataUpdate(0, type(uint256).max);
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();
Comment on lines -268 to +303
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-zero source change

}

/// @inheritdoc ERC721Upgradeable
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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;
}
Expand Down