Skip to content

Commit

Permalink
feat: implemented SPG registration with fee payer support
Browse files Browse the repository at this point in the history
This PR introduces changes that allows the fee to be paid
by the intended user while SPG performs the registration.

Description:
------------
- Added registerWithFeePayer for SPG to register IPs with user-paid fees
- Added SPG address to storage with restricted setter
- Added access control to make sure only SPG can use the new function
- Added comprehensive test suite for SPG registration flows

Testing the introduced `feat`:
------------------------------
Fetch this PR branch and from the root directory, run:
```
forge test --match-test "test_SPGRegistrationWithFeePayer|test_revert_NonSPGCannotRegisterWithFeePayer|test_revert_SPGCannotRegisterWhenFeeActive" -vv
```
  • Loading branch information
0xObsidian committed Jan 1, 2025
1 parent 23afff8 commit b5342ac
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 7 deletions.
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
50 changes: 43 additions & 7 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 @@ -86,13 +88,14 @@ contract IPAssetRegistry is
uint256 chainid,
address tokenContract,
uint256 tokenId
) external whenNotPaused returns (address id) {
id = _register({
chainid: chainid,
tokenContract: tokenContract,
tokenId: tokenId,
registerFeePayer: msg.sender
});
) external whenNotPaused returns (address) {
return
_register({
chainid: chainid,
tokenContract: tokenContract,
tokenId: tokenId,
registerFeePayer: msg.sender
});
}

function _register(
Expand Down Expand Up @@ -146,6 +149,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 +273,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);
}
}

0 comments on commit b5342ac

Please sign in to comment.