Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implemented SPG registration with fee payer support #373

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions contracts/lib/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ library Errors {
/// @notice Zero address provided for IP Asset Registry.
error IPAssetRegistry__ZeroAddress(string name);

/// @notice Thrown when the caller is not the SPG.
error IPAssetRegistry__CallerNotSPG();

/// @notice Thrown when an invalid token contract is provided.

////////////////////////////////////////////////////////////////////////////
// License Registry //
////////////////////////////////////////////////////////////////////////////
Expand Down
35 changes: 35 additions & 0 deletions contracts/registries/IPAssetRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ contract IPAssetRegistry is
/// @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.
/// @param spg The address of the SPG.
/// @custom:storage-location erc7201:story-protocol.IPAssetRegistry
struct IPAssetRegistryStorage {
uint256 totalSupply;
address treasury;
address feeToken;
uint96 feeAmount;
address spg;
}

// keccak256(abi.encode(uint256(keccak256("story-protocol.IPAssetRegistry")) - 1)) & ~bytes32(uint256(0xff));
Expand Down Expand Up @@ -146,6 +148,37 @@ contract IPAssetRegistry is
emit RegistrationFeeSet(treasury, feeToken, feeAmount);
}

/// @notice Sets the SPG address.
/// @param spg The address of the SPG.
function setSPG(address spg) external restricted {
if (spg == address(0)) revert Errors.IPAssetRegistry__ZeroAddress("spg");
IPAssetRegistryStorage storage $ = _getIPAssetRegistryStorage();
$.spg = spg;
}

/// @notice Registers an IP on behalf of a user through SPG, with the user paying the fee.
/// @param chainid The chain identifier of where the IP NFT resides.
/// @param tokenContract The address of the NFT.
/// @param tokenId The token identifier of the NFT.
/// @param feePayer The address that will pay the registration fee.
/// @return id The address of the newly registered IP.
function registerWithFeePayer(
uint256 chainid,
address tokenContract,
uint256 tokenId,
address feePayer
) external returns (address) {
IPAssetRegistryStorage storage $ = _getIPAssetRegistryStorage();

// Only SPG can call this function
if (msg.sender != address($.spg)) {
revert Errors.IPAssetRegistry__CallerNotSPG();
}

return
_register({ chainid: chainid, tokenContract: tokenContract, tokenId: tokenId, registerFeePayer: feePayer });
}

/// @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 Down Expand Up @@ -239,4 +272,6 @@ contract IPAssetRegistry is
$.slot := IPAssetRegistryStorageLocation
}
}

error SPGRegistrationWithFeeNotImplemented();
}
78 changes: 78 additions & 0 deletions test/foundry/registries/IPAssetRegistry.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ contract IPAssetRegistryTest is BaseTest {

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

address internal treasury;
address internal spg;
uint96 internal constant FEE_AMOUNT = 1000;

/// @notice Initializes the IP asset registry testing contract.
function setUp() public virtual override {
super.setUp();
Expand All @@ -56,6 +60,25 @@ contract IPAssetRegistryTest is BaseTest {
ipId = _getIPAccount(block.chainid, tokenId);
}

function _setupRegistrationFee() internal {
address treasury = makeAddr("treasury");
vm.prank(u.admin);
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);
}

function _setupSPG() internal returns (address) {
address spg = makeAddr("spg");
vm.prank(u.admin);
registry.setSPG(spg);
return spg;
}

function _setupFeePayer(address payer) internal {
erc20.mint(payer, FEE_AMOUNT);
vm.prank(payer);
erc20.approve(address(registry), FEE_AMOUNT);
}

/// @notice Tests retrieval of IP canonical IDs.
function test_IPAssetRegistry_IpId() public {
assertEq(registry.ipId(block.chainid, tokenAddress, tokenId), _getIPAccount(block.chainid, tokenId));
Expand Down Expand Up @@ -420,4 +443,59 @@ contract IPAssetRegistryTest is BaseTest {
function _toBytes32(address a) internal pure returns (bytes32) {
return bytes32(uint256(uint160(a)));
}

function test_SPGRegistrationWithFeePayer() public {
// Setup
address treasury = makeAddr("treasury");
address spg = makeAddr("spg");
address user = makeAddr("user");

vm.startPrank(u.admin);
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);
registry.setSPG(spg);
vm.stopPrank();

erc20.mint(user, FEE_AMOUNT);
vm.prank(user);
erc20.approve(address(registry), FEE_AMOUNT);

// Test
vm.prank(spg);
address registeredId = registry.registerWithFeePayer(block.chainid, tokenAddress, tokenId, user);

assertTrue(registry.isRegistered(registeredId));
assertEq(erc20.balanceOf(treasury), FEE_AMOUNT);
assertEq(erc20.balanceOf(user), 0);
}

function test_revert_NonSPGCannotRegisterWithFeePayer() public {
// Setup
address treasury = makeAddr("treasury");
address spg = makeAddr("spg");

vm.startPrank(u.admin);
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);
registry.setSPG(spg);
vm.stopPrank();

// Test
address nonSpg = makeAddr("nonSpg");
vm.prank(nonSpg);
vm.expectRevert(Errors.IPAssetRegistry__CallerNotSPG.selector);
registry.registerWithFeePayer(block.chainid, tokenAddress, tokenId, makeAddr("user"));
}

function test_revert_SPGCannotRegisterWhenFeeActive() public {
// Setup
address treasury = makeAddr("treasury");
address spg = makeAddr("spg");

vm.prank(u.admin);
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);

// Test
vm.prank(spg);
vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientAllowance.selector, address(registry), 0, FEE_AMOUNT));
registry.register(block.chainid, tokenAddress, tokenId);
}
}