Skip to content

Commit b5342ac

Browse files
committed
feat: implemented SPG registration with fee payer support
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 ```
1 parent 23afff8 commit b5342ac

File tree

3 files changed

+126
-7
lines changed

3 files changed

+126
-7
lines changed

contracts/lib/Errors.sol

+5
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ library Errors {
158158
/// @notice Zero address provided for IP Asset Registry.
159159
error IPAssetRegistry__ZeroAddress(string name);
160160

161+
/// @notice Thrown when the caller is not the SPG.
162+
error IPAssetRegistry__CallerNotSPG();
163+
164+
/// @notice Thrown when an invalid token contract is provided.
165+
161166
////////////////////////////////////////////////////////////////////////////
162167
// License Registry //
163168
////////////////////////////////////////////////////////////////////////////

contracts/registries/IPAssetRegistry.sol

+43-7
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ contract IPAssetRegistry is
4444
/// @param treasury The address of the treasury that receives registration fees.
4545
/// @param feeToken The address of the token used to pay registration fees.
4646
/// @param feeAmount The amount of the registration fee.
47+
/// @param spg The address of the SPG.
4748
/// @custom:storage-location erc7201:story-protocol.IPAssetRegistry
4849
struct IPAssetRegistryStorage {
4950
uint256 totalSupply;
5051
address treasury;
5152
address feeToken;
5253
uint96 feeAmount;
54+
address spg;
5355
}
5456

5557
// keccak256(abi.encode(uint256(keccak256("story-protocol.IPAssetRegistry")) - 1)) & ~bytes32(uint256(0xff));
@@ -86,13 +88,14 @@ contract IPAssetRegistry is
8688
uint256 chainid,
8789
address tokenContract,
8890
uint256 tokenId
89-
) external whenNotPaused returns (address id) {
90-
id = _register({
91-
chainid: chainid,
92-
tokenContract: tokenContract,
93-
tokenId: tokenId,
94-
registerFeePayer: msg.sender
95-
});
91+
) external whenNotPaused returns (address) {
92+
return
93+
_register({
94+
chainid: chainid,
95+
tokenContract: tokenContract,
96+
tokenId: tokenId,
97+
registerFeePayer: msg.sender
98+
});
9699
}
97100

98101
function _register(
@@ -146,6 +149,37 @@ contract IPAssetRegistry is
146149
emit RegistrationFeeSet(treasury, feeToken, feeAmount);
147150
}
148151

152+
/// @notice Sets the SPG address.
153+
/// @param spg The address of the SPG.
154+
function setSPG(address spg) external restricted {
155+
if (spg == address(0)) revert Errors.IPAssetRegistry__ZeroAddress("spg");
156+
IPAssetRegistryStorage storage $ = _getIPAssetRegistryStorage();
157+
$.spg = spg;
158+
}
159+
160+
/// @notice Registers an IP on behalf of a user through SPG, with the user paying the fee.
161+
/// @param chainid The chain identifier of where the IP NFT resides.
162+
/// @param tokenContract The address of the NFT.
163+
/// @param tokenId The token identifier of the NFT.
164+
/// @param feePayer The address that will pay the registration fee.
165+
/// @return id The address of the newly registered IP.
166+
function registerWithFeePayer(
167+
uint256 chainid,
168+
address tokenContract,
169+
uint256 tokenId,
170+
address feePayer
171+
) external returns (address) {
172+
IPAssetRegistryStorage storage $ = _getIPAssetRegistryStorage();
173+
174+
// Only SPG can call this function
175+
if (msg.sender != address($.spg)) {
176+
revert Errors.IPAssetRegistry__CallerNotSPG();
177+
}
178+
179+
return
180+
_register({ chainid: chainid, tokenContract: tokenContract, tokenId: tokenId, registerFeePayer: feePayer });
181+
}
182+
149183
/// @notice Gets the canonical IP identifier associated with an IP NFT.
150184
/// @dev This is equivalent to the address of its bound IP account.
151185
/// @param chainId The chain identifier of where the IP resides.
@@ -239,4 +273,6 @@ contract IPAssetRegistry is
239273
$.slot := IPAssetRegistryStorageLocation
240274
}
241275
}
276+
277+
error SPGRegistrationWithFeeNotImplemented();
242278
}

test/foundry/registries/IPAssetRegistry.t.sol

+78
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ contract IPAssetRegistryTest is BaseTest {
4343

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

46+
address internal treasury;
47+
address internal spg;
48+
uint96 internal constant FEE_AMOUNT = 1000;
49+
4650
/// @notice Initializes the IP asset registry testing contract.
4751
function setUp() public virtual override {
4852
super.setUp();
@@ -56,6 +60,25 @@ contract IPAssetRegistryTest is BaseTest {
5660
ipId = _getIPAccount(block.chainid, tokenId);
5761
}
5862

63+
function _setupRegistrationFee() internal {
64+
address treasury = makeAddr("treasury");
65+
vm.prank(u.admin);
66+
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);
67+
}
68+
69+
function _setupSPG() internal returns (address) {
70+
address spg = makeAddr("spg");
71+
vm.prank(u.admin);
72+
registry.setSPG(spg);
73+
return spg;
74+
}
75+
76+
function _setupFeePayer(address payer) internal {
77+
erc20.mint(payer, FEE_AMOUNT);
78+
vm.prank(payer);
79+
erc20.approve(address(registry), FEE_AMOUNT);
80+
}
81+
5982
/// @notice Tests retrieval of IP canonical IDs.
6083
function test_IPAssetRegistry_IpId() public {
6184
assertEq(registry.ipId(block.chainid, tokenAddress, tokenId), _getIPAccount(block.chainid, tokenId));
@@ -420,4 +443,59 @@ contract IPAssetRegistryTest is BaseTest {
420443
function _toBytes32(address a) internal pure returns (bytes32) {
421444
return bytes32(uint256(uint160(a)));
422445
}
446+
447+
function test_SPGRegistrationWithFeePayer() public {
448+
// Setup
449+
address treasury = makeAddr("treasury");
450+
address spg = makeAddr("spg");
451+
address user = makeAddr("user");
452+
453+
vm.startPrank(u.admin);
454+
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);
455+
registry.setSPG(spg);
456+
vm.stopPrank();
457+
458+
erc20.mint(user, FEE_AMOUNT);
459+
vm.prank(user);
460+
erc20.approve(address(registry), FEE_AMOUNT);
461+
462+
// Test
463+
vm.prank(spg);
464+
address registeredId = registry.registerWithFeePayer(block.chainid, tokenAddress, tokenId, user);
465+
466+
assertTrue(registry.isRegistered(registeredId));
467+
assertEq(erc20.balanceOf(treasury), FEE_AMOUNT);
468+
assertEq(erc20.balanceOf(user), 0);
469+
}
470+
471+
function test_revert_NonSPGCannotRegisterWithFeePayer() public {
472+
// Setup
473+
address treasury = makeAddr("treasury");
474+
address spg = makeAddr("spg");
475+
476+
vm.startPrank(u.admin);
477+
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);
478+
registry.setSPG(spg);
479+
vm.stopPrank();
480+
481+
// Test
482+
address nonSpg = makeAddr("nonSpg");
483+
vm.prank(nonSpg);
484+
vm.expectRevert(Errors.IPAssetRegistry__CallerNotSPG.selector);
485+
registry.registerWithFeePayer(block.chainid, tokenAddress, tokenId, makeAddr("user"));
486+
}
487+
488+
function test_revert_SPGCannotRegisterWhenFeeActive() public {
489+
// Setup
490+
address treasury = makeAddr("treasury");
491+
address spg = makeAddr("spg");
492+
493+
vm.prank(u.admin);
494+
registry.setRegistrationFee(treasury, address(erc20), FEE_AMOUNT);
495+
496+
// Test
497+
vm.prank(spg);
498+
vm.expectRevert(abi.encodeWithSelector(ERC20InsufficientAllowance.selector, address(registry), 0, FEE_AMOUNT));
499+
registry.register(block.chainid, tokenAddress, tokenId);
500+
}
423501
}

0 commit comments

Comments
 (0)