Skip to content

Commit

Permalink
Introduce IP Registration Fee Switch and Governance Controls (#271)
Browse files Browse the repository at this point in the history
  • Loading branch information
kingster-will authored Oct 15, 2024
1 parent 9222d35 commit 58f023c
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 1 deletion.
36 changes: 36 additions & 0 deletions contracts/interfaces/registries/IIPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ interface IIPAssetRegistry is IIPAccountRegistry {
uint256 registrationDate
);

/// @notice Emits when an IP registration fee is paid.
/// @param payer The address of the account that paid the fee.
/// @param treasury The address of the treasury that received the fee.
/// @param feeToken The address of the token used to pay the fee.
/// @param amount The amount of the fee paid.
event IPRegistrationFeePaid(
address indexed payer,
address indexed treasury,
address indexed feeToken,
uint96 amount
);

/// @notice Emits when an IP registration fee is set.
/// @param treasury The address of the treasury that will receive the fee.
/// @param feeToken The address of the token used to pay the fee.
/// @param feeAmount The amount of the fee.
event RegistrationFeeSet(address indexed treasury, address indexed feeToken, uint96 feeAmount);

/// @notice Sets the registration fee for IP assets.
/// @param treasury The address of the treasury that will receive the fee.
/// @param feeToken The address of the token used to pay the fee.
/// @param feeAmount The amount of the fee.
function setRegistrationFee(address treasury, address feeToken, uint96 feeAmount) external;

/// @notice Tracks the total number of IP assets in existence.
function totalSupply() external view returns (uint256);

Expand All @@ -46,4 +70,16 @@ interface IIPAssetRegistry is IIPAccountRegistry {
/// @param id The canonical identifier for the IP.
/// @return isRegistered Whether the IP was registered into the protocol.
function isRegistered(address id) external view returns (bool);

/// @notice Retrieves the treasury address for IP assets.
/// @return treasury The address of the treasury.
function getTreasury() external view returns (address);

/// @notice Retrieves the registration fee token for IP assets.
/// @return feeToken The address of the token used to pay the fee.
function getFeeToken() external view returns (address);

/// @notice Retrieves the registration fee amount for IP assets.
/// @return feeAmount The amount of the fee.
function getFeeAmount() external view returns (uint96);
}
3 changes: 3 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ library Errors {
/// @notice The NFT token id does not exist or invalid.
error IPAssetRegistry__InvalidToken(address contractAddress, uint256 tokenId);

/// @notice Zero address provided for IP Asset Registry.
error IPAssetRegistry__ZeroAddress(string name);

////////////////////////////////////////////////////////////////////////////
// License Registry //
////////////////////////////////////////////////////////////////////////////
Expand Down
57 changes: 56 additions & 1 deletion contracts/registries/IPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions
import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { IIPAccount } from "../interfaces/IIPAccount.sol";
import { GroupIPAssetRegistry } from "./GroupIPAssetRegistry.sol";
Expand Down Expand Up @@ -34,12 +36,20 @@ contract IPAssetRegistry is
using ERC165Checker for address;
using Strings for *;
using IPAccountStorageOps for IIPAccount;
using SafeERC20 for IERC20;

/// @dev Storage structure for the IPAssetRegistry
/// @notice Tracks the total number of IP assets in existence.
/// @param totalSupply The total number of IP assets registered.
/// @param treasury The address of the treasury that receives registration fees.
/// @param feeToken The address of the token used to pay registration fees.
/// @param feeAmount The amount of the registration fee.
/// @custom:storage-location erc7201:story-protocol.IPAssetRegistry
struct IPAssetRegistryStorage {
uint256 totalSupply;
address treasury;
address feeToken;
uint96 feeAmount;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol.IPAssetRegistry")) - 1)) & ~bytes32(uint256(0xff));
Expand Down Expand Up @@ -80,6 +90,17 @@ contract IPAssetRegistry is
}

function _register(uint256 chainid, address tokenContract, uint256 tokenId) internal override returns (address id) {
IPAssetRegistryStorage storage $ = _getIPAssetRegistryStorage();

// Pay registration fee
uint96 feeAmount = $.feeAmount;
if (feeAmount > 0) {
address feeToken = $.feeToken;
address treasury = $.treasury;
IERC20(feeToken).safeTransferFrom(msg.sender, treasury, uint256(feeAmount));
emit IPRegistrationFeePaid(msg.sender, treasury, feeToken, feeAmount);
}

id = _registerIpAccount(chainid, tokenContract, tokenId);
IIPAccount ipAccount = IIPAccount(payable(id));

Expand All @@ -93,11 +114,27 @@ contract IPAssetRegistry is
ipAccount.setString("URI", uri);
ipAccount.setUint256("REGISTRATION_DATE", registrationDate);

_getIPAssetRegistryStorage().totalSupply++;
$.totalSupply++;

emit IPRegistered(id, chainid, tokenContract, tokenId, name, uri, registrationDate);
}

/// @notice Sets the registration fee for IP assets.
/// @param treasury The address of the treasury that will receive the fee.
/// @param feeToken The address of the token used to pay the fee.
/// @param feeAmount The amount of the fee.
function setRegistrationFee(address treasury, address feeToken, uint96 feeAmount) external restricted {
if (feeAmount > 0) {
if (treasury == address(0)) revert Errors.IPAssetRegistry__ZeroAddress("treasury");
if (feeToken == address(0)) revert Errors.IPAssetRegistry__ZeroAddress("feeToken");
}
IPAssetRegistryStorage storage $ = _getIPAssetRegistryStorage();
$.feeToken = feeToken;
$.feeAmount = feeAmount;
$.treasury = treasury;
emit RegistrationFeeSet(treasury, feeToken, feeAmount);
}

/// @notice Gets the canonical IP identifier associated with an IP NFT.
/// @dev This is equivalent to the address of its bound IP account.
/// @param chainId The chain identifier of where the IP resides.
Expand All @@ -120,6 +157,24 @@ contract IPAssetRegistry is
return _getIPAssetRegistryStorage().totalSupply;
}

/// @notice Retrieves the treasury address for IP assets.
/// @return treasury The address of the treasury.
function getTreasury() external view returns (address) {
return _getIPAssetRegistryStorage().treasury;
}

/// @notice Retrieves the registration fee token for IP assets.
/// @return feeToken The address of the token used to pay the fee.
function getFeeToken() external view returns (address) {
return _getIPAssetRegistryStorage().feeToken;
}

/// @notice Retrieves the registration fee amount for IP assets.
/// @return feeAmount The amount of the fee.
function getFeeAmount() external view returns (uint96) {
return _getIPAssetRegistryStorage().feeAmount;
}

/// @dev Retrieves the name and URI of from IP NFT.
function _getNameAndUri(
uint256 chainid,
Expand Down
100 changes: 100 additions & 0 deletions test/foundry/registries/IPAssetRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ contract IPAssetRegistryTest is BaseTest {
uint256 public tokenId;
address public ipId;

error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);

/// @notice Initializes the IP asset registry testing contract.
function setUp() public virtual override {
super.setUp();
Expand Down Expand Up @@ -77,6 +79,104 @@ contract IPAssetRegistryTest is BaseTest {
assertEq(IIPAccount(payable(ipId)).getUint256(address(registry), "REGISTRATION_DATE"), block.timestamp);
}

function test_IPAssetRegistry_SetRegisterFee() public {
address treasury = address(0x123);
vm.prank(u.admin);
vm.expectEmit();
emit IIPAssetRegistry.RegistrationFeeSet(treasury, address(erc20), 1000);
registry.setRegistrationFee(treasury, address(erc20), 1000);

assertEq(registry.getTreasury(), treasury, "Treasury not set");
assertEq(registry.getFeeToken(), address(erc20), "Fee token not set");
assertEq(registry.getFeeAmount(), 1000, "Fee amount not set");
}

function test_IPAssetRegistry_revert_SetRegisterFeeZeroTreasury() public {
vm.expectRevert(abi.encodeWithSelector(Errors.IPAssetRegistry__ZeroAddress.selector, "treasury"));
vm.prank(u.admin);
registry.setRegistrationFee(address(0), address(erc20), 1000);
}

function test_IPAssetRegistry_revert_SetRegisterFeeZeroTokenAddress() public {
vm.expectRevert(abi.encodeWithSelector(Errors.IPAssetRegistry__ZeroAddress.selector, "feeToken"));
vm.prank(u.admin);
registry.setRegistrationFee(address(0x123), address(0), 1000);
}

function test_IPAssetRegistry_resetRegisterFeeBackToZero() public {
address treasury = address(0x123);
vm.prank(u.admin);
vm.expectEmit();
emit IIPAssetRegistry.RegistrationFeeSet(treasury, address(erc20), 1000);
registry.setRegistrationFee(treasury, address(erc20), 1000);

assertEq(registry.getTreasury(), treasury, "Treasury not set");
assertEq(registry.getFeeToken(), address(erc20), "Fee token not set");
assertEq(registry.getFeeAmount(), 1000, "Fee amount not set");

vm.prank(u.admin);
vm.expectEmit();
emit IIPAssetRegistry.RegistrationFeeSet(address(0), address(0), 0);
registry.setRegistrationFee(address(0), address(0), 0);

assertEq(registry.getTreasury(), address(0), "Treasury not reset");
assertEq(registry.getFeeToken(), address(0), "Fee token not reset");
assertEq(registry.getFeeAmount(), 0, "Fee amount not reset");
}

/// @notice Tests registration of IP permissionlessly.
function test_IPAssetRegistry_RegisterWithPayRegisterFee() public {
address treasury = address(0x123);
vm.prank(u.admin);
vm.expectEmit();
emit IIPAssetRegistry.RegistrationFeeSet(treasury, address(erc20), 1000);
registry.setRegistrationFee(treasury, address(erc20), 1000);

erc20.mint(alice, 1000);
vm.prank(alice);
erc20.approve(address(registry), 1000);

uint256 totalSupply = registry.totalSupply();

string memory name = string.concat(block.chainid.toString(), ": Ape #99");
vm.expectEmit(true, true, true, true);
emit IIPAssetRegistry.IPRegistrationFeePaid(alice, treasury, address(erc20), 1000);
emit IIPAssetRegistry.IPRegistered(
ipId,
block.chainid,
tokenAddress,
tokenId,
name,
"https://storyprotocol.xyz/erc721/99",
block.timestamp
);
vm.prank(alice);
registry.register(block.chainid, tokenAddress, tokenId);

assertEq(totalSupply + 1, registry.totalSupply());
assertTrue(IPAccountChecker.isRegistered(ipAccountRegistry, block.chainid, tokenAddress, tokenId));
assertEq(IIPAccount(payable(ipId)).getString(address(registry), "NAME"), name);
assertEq(IIPAccount(payable(ipId)).getString(address(registry), "URI"), "https://storyprotocol.xyz/erc721/99");
assertEq(IIPAccount(payable(ipId)).getUint256(address(registry), "REGISTRATION_DATE"), block.timestamp);
}

/// @notice Tests registration of IP permissionlessly.
function test_IPAssetRegistry_revert_RegisterNotEnoughRegisterFee() public {
address treasury = address(0x123);
vm.prank(u.admin);
vm.expectEmit();
emit IIPAssetRegistry.RegistrationFeeSet(treasury, address(erc20), 1000);
registry.setRegistrationFee(treasury, address(erc20), 1000);

erc20.mint(alice, 100);
vm.prank(alice);
erc20.approve(address(registry), 100);

vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientAllowance.selector, address(registry), 100, 1000));
vm.prank(alice);
registry.register(block.chainid, tokenAddress, tokenId);
}

/// @notice Tests registration of IP permissionlessly for IPAccount already created.
function test_IPAssetRegistry_RegisterPermissionless_IPAccountAlreadyExist() public {
uint256 totalSupply = registry.totalSupply();
Expand Down

0 comments on commit 58f023c

Please sign in to comment.