From d5fa6cdbd18f0bd4094c34bcbb40b665bd617198 Mon Sep 17 00:00:00 2001 From: luzzif Date: Fri, 9 May 2025 13:14:36 +0100 Subject: [PATCH 1/3] feat: introduce reimbursement fee --- src/IMetrom.sol | 80 ++++++++++++++----- src/Metrom.sol | 66 +++++++++++---- test/Base.t.sol | 9 ++- test/ClaimRecoverRewards.sol | 16 +++- test/ClaimRewards.sol | 24 ++++-- test/ClaimedCampaignReward.sol | 8 +- test/CreatePointsCampaign.sol | 2 +- test/CreateRewardsCampaign.sol | 2 +- test/DistributeRewards.sol | 76 ++++++++++++++++-- test/Initialize.t.sol | 52 ++++++++---- test/{SetFee.t.sol => SetCreationFee.t.sol} | 18 ++--- ...ebate.t.sol => SetCreationFeeRebate.t.sol} | 26 +++--- test/SetReimbursementFee.t.sol | 40 ++++++++++ test/SetReimbursementFeeRebate.t.sol | 58 ++++++++++++++ test/Upgrade.t.sol | 2 +- 15 files changed, 377 insertions(+), 102 deletions(-) rename test/{SetFee.t.sol => SetCreationFee.t.sol} (62%) rename test/{SetFeeRebate.t.sol => SetCreationFeeRebate.t.sol} (57%) create mode 100644 test/SetReimbursementFee.t.sol create mode 100644 test/SetReimbursementFeeRebate.t.sol diff --git a/src/IMetrom.sol b/src/IMetrom.sol index 23dead6..3fd513b 100644 --- a/src/IMetrom.sol +++ b/src/IMetrom.sol @@ -143,11 +143,13 @@ struct CreatePointsCampaignBundle { } /// @notice Contains data that can be used by the current `updater` to -/// distribute rewards on a campaign by specifying a Merkle root and a data link. +/// distribute rewards on a campaign by specifying a Merkle root, a data link, +/// and an optional list of reimbursement fees. struct DistributeRewardsBundle { bytes32 campaignId; bytes32 root; bytes32 dataHash; + RewardAmount[] reimbursementFees; } /// @notice Contains data that can be used by the current `updater` or the @@ -185,13 +187,15 @@ interface IMetrom { /// @notice Emitted at initialization time. /// @param owner The initial contract's owner. /// @param updater The initial contract's updater. - /// @param fee The initial contract's rewards campaign fee. + /// @param creationFee The initial contract's rewards campaign creation fee. + /// @param reimbursementFee The initial contract's rewards campaign reimbursement fee. /// @param minimumCampaignDuration The initial contract's minimum campaign duration. /// @param maximumCampaignDuration The initial contract's maximum campaign duration. event Initialize( address indexed owner, address updater, - uint32 fee, + uint32 creationFee, + uint32 reimbursementFee, uint32 minimumCampaignDuration, uint32 maximumCampaignDuration ); @@ -251,7 +255,9 @@ interface IMetrom { /// @param data The updated data content hash for the campaign. This can be used to /// contruct an IPFS CID for a file that will contain the raw data used to get the raw /// data used to contruct the campaign's Merkle tree and verify the Merkle root. - event DistributeReward(bytes32 indexed campaignId, bytes32 root, bytes32 data); + /// @param reimbursementFees A list of fees applied to the campaign's reimbursed amounts + /// (for example due to a non-met KPI). + event DistributeReward(bytes32 indexed campaignId, bytes32 root, bytes32 data, RewardAmount[] reimbursementFees); /// @notice Emitted when the rates updater or the owner updates the minimum emission /// rate of a certain whitelisted reward token required in order to create a rewards based @@ -310,15 +316,27 @@ interface IMetrom { /// @param updater The new updater. event SetUpdater(address indexed updater); - /// @notice Emitted when Metrom's owner sets a new rewards based campaign fee. - /// @param fee The new rewards campaign fee. - event SetFee(uint32 fee); + /// @notice Emitted when Metrom's owner sets a new rewards based campaign + /// creation fee. + /// @param creationFee The new rewards campaign creation fee. + event SetCreationFee(uint32 creationFee); /// @notice Emitted when Metrom's owner sets a new address-specific - /// rebate for the protocol rewards based campaign fees. + /// rebate for the protocol rewards based campaign creation fees. /// @param account The account for which the rebate was set. /// @param rebate The rebate. - event SetFeeRebate(address account, uint32 rebate); + event SetCreationFeeRebate(address account, uint32 rebate); + + /// @notice Emitted when Metrom's owner sets a new rewards based campaign + /// reimbursement fee. + /// @param reimbursementFee The new rewards campaign reimbursement fee. + event SetReimbursementFee(uint32 reimbursementFee); + + /// @notice Emitted when Metrom's owner sets a new address-specific + /// rebate for the protocol rewards based campaign reimbursement fees. + /// @param account The account for which the rebate was set. + /// @param rebate The rebate. + event SetReimbursementFeeRebate(address account, uint32 rebate); /// @notice Emitted when Metrom's owner sets a new minimum campaign duration. /// @param minimumCampaignDuration The new minimum campaign duration. @@ -428,13 +446,15 @@ interface IMetrom { /// @notice Initializes the contract. /// @param owner The initial owner. /// @param updater The initial updater. - /// @param fee The initial fee. + /// @param creationFee The initial creation fee. + /// @param reimbursementFee The initial reimbursement fee. /// @param minimumCampaignDuration The initial minimum campaign duration. /// @param maximumCampaignDuration The initial maximum campaign duration. function initialize( address owner, address updater, - uint32 fee, + uint32 creationFee, + uint32 reimbursementFee, uint32 minimumCampaignDuration, uint32 maximumCampaignDuration ) external; @@ -459,14 +479,23 @@ interface IMetrom { /// @return updater The currently allowed updater. function updater() external view returns (address updater); - /// @notice Returns the current fee. - /// @return fee The current fee. - function fee() external view returns (uint32 fee); + /// @notice Returns the current creation fee. + /// @return creationFee The current creation fee. + function creationFee() external view returns (uint32 creationFee); - /// @notice Returns the current fee rebate for a provided account. + /// @notice Returns the current creation fee rebate for a provided account. /// @param account The account for which to fetch the fee rebate. - /// @return rebate The fee rebate for the provided account. - function feeRebate(address account) external view returns (uint32 rebate); + /// @return rebate The creation fee rebate for the provided account. + function creationFeeRebate(address account) external view returns (uint32 rebate); + + /// @notice Returns the current reimbursement fee. + /// @return creationFee The current reimbursement fee. + function reimbursementFee() external view returns (uint32 creationFee); + + /// @notice Returns the current reimbursement fee rebate for a provided account. + /// @param account The account for which to fetch the fee rebate. + /// @return rebate The reimbursement fee rebate for the provided account. + function reimbursementFeeRebate(address account) external view returns (uint32 rebate); /// @notice Returns the currently enforced minimum campaign duration. /// @return minimumCampaignDuration The currently enforced minimum campaign duration. @@ -595,14 +624,23 @@ interface IMetrom { /// @param updater The new updater address. function setUpdater(address updater) external; - /// @notice Can be called by Metrom's owner to set a new fee value. - function setFee(uint32 fee) external; + /// @notice Can be called by Metrom's owner to set a new creation fee value. + function setCreationFee(uint32 creationFee) external; + + /// @notice Can be called by Metrom's owner to set a new specific creation fee + /// rebate for an account. + /// @param account The account for which to set the rebate value. + /// @param rebate The rebate. + function setCreationFeeRebate(address account, uint32 rebate) external; + + /// @notice Can be called by Metrom's owner to set a new reimbursement fee value. + function setReimbursementFee(uint32 creationFee) external; - /// @notice Can be called by Metrom's owner to set a new specific protocol fee + /// @notice Can be called by Metrom's owner to set a new specific reimbursement fee /// rebate for an account. /// @param account The account for which to set the rebate value. /// @param rebate The rebate. - function setFeeRebate(address account, uint32 rebate) external; + function setReimbursementFeeRebate(address account, uint32 rebate) external; /// @notice Can be called by Metrom's owner to set a new minimum allowed campaign duration. /// @param minimumCampaignDuration The new minimum allowed campaign duration. diff --git a/src/Metrom.sol b/src/Metrom.sol index ff58b3c..e0dc012 100644 --- a/src/Metrom.sol +++ b/src/Metrom.sol @@ -19,6 +19,7 @@ import { RewardsCampaignV1, RewardsCampaignV2, Reward, + RewardAmount, PointsCampaignV1, PointsCampaignV2, ReadonlyRewardsCampaign, @@ -62,7 +63,7 @@ contract Metrom is IMetrom, UUPSUpgradeable { address public override updater; /// @inheritdoc IMetrom - uint32 public override fee; + uint32 public override creationFee; /// @inheritdoc IMetrom uint32 public override minimumCampaignDuration; @@ -73,7 +74,7 @@ contract Metrom is IMetrom, UUPSUpgradeable { RewardsCampaignsV1 internal rewardsCampaignsV1; /// @inheritdoc IMetrom - mapping(address account => uint32 rebate) public override feeRebate; + mapping(address account => uint32 creationFeeRebate) public override creationFeeRebate; /// @inheritdoc IMetrom mapping(address token => uint256 amount) public override claimableFees; @@ -90,6 +91,12 @@ contract Metrom is IMetrom, UUPSUpgradeable { PointsCampaignsV2 internal pointsCampaignsV2; + /// @inheritdoc IMetrom + uint32 public override reimbursementFee; + + /// @inheritdoc IMetrom + mapping(address account => uint32 reimbursementFeeRebate) public override reimbursementFeeRebate; + constructor() { _disableInitializers(); } @@ -98,22 +105,27 @@ contract Metrom is IMetrom, UUPSUpgradeable { function initialize( address _owner, address _updater, - uint32 _fee, + uint32 _creationFee, + uint32 _reimbursementFee, uint32 _minimumCampaignDuration, uint32 _maximumCampaignDuration ) external override initializer { if (_owner == address(0)) revert ZeroAddressOwner(); if (_updater == address(0)) revert ZeroAddressUpdater(); - if (_fee >= UNIT) revert InvalidFee(); + if (_creationFee >= UNIT) revert InvalidFee(); + if (_reimbursementFee >= UNIT) revert InvalidFee(); if (_minimumCampaignDuration >= _maximumCampaignDuration) revert InvalidMinimumCampaignDuration(); owner = _owner; updater = _updater; minimumCampaignDuration = _minimumCampaignDuration; maximumCampaignDuration = _maximumCampaignDuration; - fee = _fee; + creationFee = _creationFee; + reimbursementFee = _reimbursementFee; - emit Initialize(_owner, _updater, _fee, _minimumCampaignDuration, _maximumCampaignDuration); + emit Initialize( + _owner, _updater, _creationFee, _reimbursementFee, _minimumCampaignDuration, _maximumCampaignDuration + ); } /// @inheritdoc IMetrom @@ -189,8 +201,8 @@ contract Metrom is IMetrom, UUPSUpgradeable { CreateRewardsCampaignBundle[] calldata _rewardsCampaignBundles, CreatePointsCampaignBundle[] calldata _pointsCampaignBundles ) external { - uint32 _fee = fee; - uint32 _feeRebate = feeRebate[msg.sender]; + uint32 _fee = creationFee; + uint32 _feeRebate = creationFeeRebate[msg.sender]; uint32 _resolvedRewardsCampaignFee = uint32(uint64(_fee) * (UNIT - _feeRebate) / UNIT); uint32 _minimumCampaignDuration = minimumCampaignDuration; uint32 _maximumCampaignDuration = maximumCampaignDuration; @@ -343,7 +355,12 @@ contract Metrom is IMetrom, UUPSUpgradeable { campaignV2.dataHash = _bundle.dataHash; } - emit DistributeReward(_bundle.campaignId, _bundle.root, _bundle.dataHash); + for (uint256 _j = 0; _j < _bundle.reimbursementFees.length; _j++) { + RewardAmount calldata _reimbursementFee = _bundle.reimbursementFees[_i]; + claimableFees[_reimbursementFee.token] += _reimbursementFee.amount; + } + + emit DistributeReward(_bundle.campaignId, _bundle.root, _bundle.dataHash, _bundle.reimbursementFees); } } @@ -574,20 +591,37 @@ contract Metrom is IMetrom, UUPSUpgradeable { } /// @inheritdoc IMetrom - function setFee(uint32 _fee) external override { - if (_fee >= UNIT) revert InvalidFee(); + function setCreationFee(uint32 _creationFee) external override { + if (_creationFee >= UNIT) revert InvalidFee(); + if (msg.sender != owner) revert Forbidden(); + creationFee = _creationFee; + emit SetCreationFee(_creationFee); + } + + /// @inheritdoc IMetrom + function setCreationFeeRebate(address _account, uint32 _rebate) external override { + if (_account == address(0)) revert ZeroAddressAccount(); + if (_rebate > UNIT) revert RebateTooHigh(); + if (msg.sender != owner) revert Forbidden(); + creationFeeRebate[_account] = _rebate; + emit SetCreationFeeRebate(_account, _rebate); + } + + /// @inheritdoc IMetrom + function setReimbursementFee(uint32 _reimbursementFee) external override { + if (_reimbursementFee >= UNIT) revert InvalidFee(); if (msg.sender != owner) revert Forbidden(); - fee = _fee; - emit SetFee(_fee); + reimbursementFee = _reimbursementFee; + emit SetReimbursementFee(_reimbursementFee); } /// @inheritdoc IMetrom - function setFeeRebate(address _account, uint32 _rebate) external override { + function setReimbursementFeeRebate(address _account, uint32 _rebate) external override { if (_account == address(0)) revert ZeroAddressAccount(); if (_rebate > UNIT) revert RebateTooHigh(); if (msg.sender != owner) revert Forbidden(); - feeRebate[_account] = _rebate; - emit SetFeeRebate(_account, _rebate); + reimbursementFeeRebate[_account] = _rebate; + emit SetReimbursementFeeRebate(_account, _rebate); } /// @inheritdoc IMetrom diff --git a/test/Base.t.sol b/test/Base.t.sol index 2af39a4..b7656bd 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -17,7 +17,8 @@ import { contract BaseTest is Test { address internal owner; address internal updater; - uint32 internal fee; + uint32 internal creationFee; + uint32 internal reimbursementFee; uint32 internal minimumCampaignDuration; uint32 internal maximumCampaignDuration; MetromHarness internal metrom; @@ -25,7 +26,8 @@ contract BaseTest is Test { function setUp() external { owner = address(1); updater = address(2); - fee = 10_000; + creationFee = 10_000; + reimbursementFee = 50_000; minimumCampaignDuration = 1 seconds; maximumCampaignDuration = 30 minutes; metrom = MetromHarness( @@ -36,7 +38,8 @@ contract BaseTest is Test { IMetrom.initialize.selector, owner, updater, - fee, + creationFee, + reimbursementFee, minimumCampaignDuration, maximumCampaignDuration ) diff --git a/test/ClaimRecoverRewards.sol b/test/ClaimRecoverRewards.sol index 92e3b34..f597359 100644 --- a/test/ClaimRecoverRewards.sol +++ b/test/ClaimRecoverRewards.sol @@ -311,8 +311,12 @@ contract ClaimRecoverRewards is BaseTest { // then the provided proof at claim time is the one for the second claim bytes32 _root = bytes32(0x40e9e0b48e6b5b10bcf2b72e0c7af4fe754c94bb43ef3e668c9fb535bb3554ae); - DistributeRewardsBundle memory _distributeRewardBundle = - DistributeRewardsBundle({campaignId: _createdCampaignId, root: _root, dataHash: bytes32("foo")}); + DistributeRewardsBundle memory _distributeRewardBundle = DistributeRewardsBundle({ + campaignId: _createdCampaignId, + root: _root, + dataHash: bytes32("foo"), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _distributeRewardBundles = new DistributeRewardsBundle[](1); _distributeRewardBundles[0] = _distributeRewardBundle; @@ -401,8 +405,12 @@ contract ClaimRecoverRewards is BaseTest { // then the provided proof at claim time is the one for the second claim bytes32 _root = bytes32(0x40e9e0b48e6b5b10bcf2b72e0c7af4fe754c94bb43ef3e668c9fb535bb3554ae); - DistributeRewardsBundle memory _distributeRewardBundle = - DistributeRewardsBundle({campaignId: _createdCampaignId, root: _root, dataHash: bytes32("foo")}); + DistributeRewardsBundle memory _distributeRewardBundle = DistributeRewardsBundle({ + campaignId: _createdCampaignId, + root: _root, + dataHash: bytes32("foo"), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _distributeRewardBundles = new DistributeRewardsBundle[](1); _distributeRewardBundles[0] = _distributeRewardBundle; diff --git a/test/ClaimRewards.sol b/test/ClaimRewards.sol index 1341418..64e6c96 100644 --- a/test/ClaimRewards.sol +++ b/test/ClaimRewards.sol @@ -206,8 +206,12 @@ contract ClaimRewardsTest is BaseTest { // then the provided proof at claim time is the one for the second claim bytes32 _root = bytes32(0x8177f131454affec90ea987fa3784afc6d2ca02009c6c8f9e7e7183794f801fe); - DistributeRewardsBundle memory _distributeRewardBundle = - DistributeRewardsBundle({campaignId: _createdCampaignId, root: _root, dataHash: bytes32("foo")}); + DistributeRewardsBundle memory _distributeRewardBundle = DistributeRewardsBundle({ + campaignId: _createdCampaignId, + root: _root, + dataHash: bytes32("foo"), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _distributeRewardBundles = new DistributeRewardsBundle[](1); _distributeRewardBundles[0] = _distributeRewardBundle; @@ -353,8 +357,12 @@ contract ClaimRewardsTest is BaseTest { // then the provided proof at claim time is the one for the second claim bytes32 _root = bytes32(0xb1ba26940192dab1dbb383cfc69674e93fb8011037f51703624b66d5238661b5); - DistributeRewardsBundle memory _distributeRewardBundle = - DistributeRewardsBundle({campaignId: _createdCampaignId, root: _root, dataHash: bytes32("foo")}); + DistributeRewardsBundle memory _distributeRewardBundle = DistributeRewardsBundle({ + campaignId: _createdCampaignId, + root: _root, + dataHash: bytes32("foo"), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _distributeRewardBundles = new DistributeRewardsBundle[](1); _distributeRewardBundles[0] = _distributeRewardBundle; @@ -445,8 +453,12 @@ contract ClaimRewardsTest is BaseTest { // then the provided proof at claim time is the one for the second claim bytes32 _root = bytes32(0xb1ba26940192dab1dbb383cfc69674e93fb8011037f51703624b66d5238661b5); - DistributeRewardsBundle memory _distributeRewardBundle = - DistributeRewardsBundle({campaignId: _createdCampaignId, root: _root, dataHash: bytes32("foo")}); + DistributeRewardsBundle memory _distributeRewardBundle = DistributeRewardsBundle({ + campaignId: _createdCampaignId, + root: _root, + dataHash: bytes32("foo"), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _distributeRewardBundles = new DistributeRewardsBundle[](1); _distributeRewardBundles[0] = _distributeRewardBundle; diff --git a/test/ClaimedCampaignReward.sol b/test/ClaimedCampaignReward.sol index a8be2dc..fd71e3d 100644 --- a/test/ClaimedCampaignReward.sol +++ b/test/ClaimedCampaignReward.sol @@ -67,8 +67,12 @@ contract ClaimedCampaignRewardTest is BaseTest { bytes32 _createdCampaignId = metrom.rewardsCampaignId(_createRewardsCampaignBundle); { bytes32 _root = bytes32(0xb1ba26940192dab1dbb383cfc69674e93fb8011037f51703624b66d5238661b5); - DistributeRewardsBundle memory _distributeRewardBundle = - DistributeRewardsBundle({campaignId: _createdCampaignId, root: _root, dataHash: bytes32("foo")}); + DistributeRewardsBundle memory _distributeRewardBundle = DistributeRewardsBundle({ + campaignId: _createdCampaignId, + root: _root, + dataHash: bytes32("foo"), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _distributeRewardBundles = new DistributeRewardsBundle[](1); _distributeRewardBundles[0] = _distributeRewardBundle; diff --git a/test/CreatePointsCampaign.sol b/test/CreatePointsCampaign.sol index 2a3fc30..92b4f12 100644 --- a/test/CreatePointsCampaign.sol +++ b/test/CreatePointsCampaign.sol @@ -220,7 +220,7 @@ contract CreatePointsCampaignTest is BaseTest { _createPointsCampaignBundles[0] = _createPointsCampaignBundle; vm.prank(owner); - metrom.setFeeRebate(address(this), UNIT); + metrom.setCreationFeeRebate(address(this), UNIT); metrom.createCampaigns(new CreateRewardsCampaignBundle[](0), _createPointsCampaignBundles); diff --git a/test/CreateRewardsCampaign.sol b/test/CreateRewardsCampaign.sol index 5e6ffc0..9fbd689 100644 --- a/test/CreateRewardsCampaign.sol +++ b/test/CreateRewardsCampaign.sol @@ -570,7 +570,7 @@ contract CreateRewardsCampaignTest is BaseTest { _createRewardsCampaignBundles[0] = _createRewardsCampaignBundle; vm.prank(owner); - metrom.setFeeRebate(address(this), UNIT); + metrom.setCreationFeeRebate(address(this), UNIT); CreatePointsCampaignBundle[] memory _createPointsCampaignBundless = new CreatePointsCampaignBundle[](0); diff --git a/test/DistributeRewards.sol b/test/DistributeRewards.sol index e00197f..9636604 100644 --- a/test/DistributeRewards.sol +++ b/test/DistributeRewards.sol @@ -28,8 +28,12 @@ contract DistributeRewardsTest is BaseTest { } function test_failZeroRoot() public { - DistributeRewardsBundle memory _bundle = - DistributeRewardsBundle({campaignId: bytes32(0), root: bytes32(0), dataHash: bytes32(0)}); + DistributeRewardsBundle memory _bundle = DistributeRewardsBundle({ + campaignId: bytes32(0), + root: bytes32(0), + dataHash: bytes32(0), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _bundles = new DistributeRewardsBundle[](1); _bundles[0] = _bundle; @@ -40,8 +44,12 @@ contract DistributeRewardsTest is BaseTest { } function test_failZeroData() public { - DistributeRewardsBundle memory _bundle = - DistributeRewardsBundle({campaignId: bytes32(0), root: bytes32("test"), dataHash: bytes32(0)}); + DistributeRewardsBundle memory _bundle = DistributeRewardsBundle({ + campaignId: bytes32(0), + root: bytes32("test"), + dataHash: bytes32(0), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _bundles = new DistributeRewardsBundle[](1); _bundles[0] = _bundle; @@ -52,8 +60,12 @@ contract DistributeRewardsTest is BaseTest { } function test_failNonExistentCampaign() public { - DistributeRewardsBundle memory _bundle = - DistributeRewardsBundle({campaignId: bytes32(0), root: bytes32("test"), dataHash: bytes32("test")}); + DistributeRewardsBundle memory _bundle = DistributeRewardsBundle({ + campaignId: bytes32(0), + root: bytes32("test"), + dataHash: bytes32("test"), + reimbursementFees: new RewardAmount[](0) + }); DistributeRewardsBundle[] memory _bundles = new DistributeRewardsBundle[](1); _bundles[0] = _bundle; @@ -92,15 +104,63 @@ contract DistributeRewardsTest is BaseTest { DistributeRewardsBundle memory _bundle = DistributeRewardsBundle({ campaignId: metrom.rewardsCampaignId(_createRewardsCampaignBundle), root: bytes32("test"), - dataHash: bytes32("test") + dataHash: bytes32("test"), + reimbursementFees: new RewardAmount[](0) + }); + DistributeRewardsBundle[] memory _bundles = new DistributeRewardsBundle[](1); + _bundles[0] = _bundle; + + vm.expectEmit(); + emit IMetrom.DistributeReward(_bundle.campaignId, _bundle.root, _bundle.dataHash, new RewardAmount[](0)); + + vm.prank(updater); + metrom.distributeRewards(_bundles); + } + + function test_successSingleCampaignWithReimbursementFees() public { + MintableERC20 _mintableErc20 = new MintableERC20("Test", "TST"); + _mintableErc20.mint(address(this), 10.1 ether); + _mintableErc20.approve(address(metrom), 10.1 ether); + vm.assertEq(_mintableErc20.balanceOf(address(this)), 10.1 ether); + setMinimumRewardRate(address(_mintableErc20), 1); + + RewardAmount[] memory _rewards = new RewardAmount[](1); + _rewards[0] = RewardAmount({token: address(_mintableErc20), amount: 10 ether}); + + CreateRewardsCampaignBundle memory _createRewardsCampaignBundle = CreateRewardsCampaignBundle({ + from: uint32(block.timestamp + 10), + to: uint32(block.timestamp + 20), + kind: 1, + data: abi.encode(address(1)), + specificationHash: bytes32(0), + rewards: _rewards + }); + + CreateRewardsCampaignBundle[] memory _createRewardsCampaignBundles = new CreateRewardsCampaignBundle[](1); + _createRewardsCampaignBundles[0] = _createRewardsCampaignBundle; + + CreatePointsCampaignBundle[] memory _createPointsCampaignBundles = new CreatePointsCampaignBundle[](0); + + metrom.createCampaigns(_createRewardsCampaignBundles, _createPointsCampaignBundles); + + RewardAmount[] memory _reimbursementFees = new RewardAmount[](1); + _reimbursementFees[0] = RewardAmount({token: address(_mintableErc20), amount: 2 ether}); + + DistributeRewardsBundle memory _bundle = DistributeRewardsBundle({ + campaignId: metrom.rewardsCampaignId(_createRewardsCampaignBundle), + root: bytes32("test"), + dataHash: bytes32("test"), + reimbursementFees: _reimbursementFees }); DistributeRewardsBundle[] memory _bundles = new DistributeRewardsBundle[](1); _bundles[0] = _bundle; vm.expectEmit(); - emit IMetrom.DistributeReward(_bundle.campaignId, _bundle.root, _bundle.dataHash); + emit IMetrom.DistributeReward(_bundle.campaignId, _bundle.root, _bundle.dataHash, _reimbursementFees); vm.prank(updater); metrom.distributeRewards(_bundles); + + vm.assertEq(metrom.claimableFees(address(_mintableErc20)), 2.1 ether); } } diff --git a/test/Initialize.t.sol b/test/Initialize.t.sol index 4e08e8f..e108bde 100644 --- a/test/Initialize.t.sol +++ b/test/Initialize.t.sol @@ -12,25 +12,31 @@ contract InitializeTest is BaseTest { function test_failOnLogicContractDirectly() public { MetromHarness _metrom = new MetromHarness(); vm.expectRevert(Initializable.InvalidInitialization.selector); - _metrom.initialize(address(1), address(1), 10, 10, 11); + _metrom.initialize(address(1), address(1), 10, 10, 10, 11); } function test_failZeroAddressOwner() public { MetromHarness _metrom = MetromHarness(address(new ERC1967Proxy(address(new MetromHarness()), bytes("")))); vm.expectRevert(IMetrom.ZeroAddressOwner.selector); - _metrom.initialize(address(0), address(0), 10, 10, 10); + _metrom.initialize(address(0), address(0), 10, 10, 10, 10); } function test_failZeroAddressUpdater() public { MetromHarness _metrom = MetromHarness(address(new ERC1967Proxy(address(new MetromHarness()), bytes("")))); vm.expectRevert(IMetrom.ZeroAddressUpdater.selector); - _metrom.initialize(address(1), address(0), 10, 10, 10); + _metrom.initialize(address(1), address(0), 10, 10, 10, 10); } - function test_failInvalidFee() public { + function test_failInvalidCreationFee() public { MetromHarness _metrom = MetromHarness(address(new ERC1967Proxy(address(new MetromHarness()), bytes("")))); vm.expectRevert(IMetrom.InvalidFee.selector); - _metrom.initialize(address(1), address(1), uint32(UNIT), 10, 10); + _metrom.initialize(address(1), address(1), uint32(UNIT), 10, 10, 10); + } + + function test_failInvalidReimbursementFee() public { + MetromHarness _metrom = MetromHarness(address(new ERC1967Proxy(address(new MetromHarness()), bytes("")))); + vm.expectRevert(IMetrom.InvalidFee.selector); + _metrom.initialize(address(1), address(1), 10, uint32(UNIT), 10, 10); } function test_failInvalidMinimumCampaignDuration() public { @@ -38,32 +44,38 @@ contract InitializeTest is BaseTest { // minimum campaign duration > than maximum campaign duration vm.expectRevert(IMetrom.InvalidMinimumCampaignDuration.selector); - _metrom.initialize(address(1), address(1), uint32(10_000), 10, 8); + _metrom.initialize(address(1), address(1), uint32(10_000), 10, 10, 8); _metrom = MetromHarness(address(new ERC1967Proxy(address(new MetromHarness()), bytes("")))); // minimum campaign duration == than maximum campaign duration vm.expectRevert(IMetrom.InvalidMinimumCampaignDuration.selector); - _metrom.initialize(address(1), address(1), uint32(10_000), 10, 10); + _metrom.initialize(address(1), address(1), uint32(10_000), 10, 10, 10); } function test_success() public { address _owner = address(1); address _updater = address(2); - uint32 _fee = 10; + uint32 _creationFee = 10; + uint32 _reimbursementFee = 20; uint32 _minimumCampaignDuration = 5 seconds; uint32 _maximumCampaignDuration = 10 seconds; MetromHarness _metrom = MetromHarness(address(new ERC1967Proxy(address(new MetromHarness()), bytes("")))); vm.expectEmit(); - emit IMetrom.Initialize(_owner, _updater, _fee, _minimumCampaignDuration, _maximumCampaignDuration); + emit IMetrom.Initialize( + _owner, _updater, _creationFee, _reimbursementFee, _minimumCampaignDuration, _maximumCampaignDuration + ); - _metrom.initialize(_owner, _updater, _fee, _minimumCampaignDuration, _maximumCampaignDuration); + _metrom.initialize( + _owner, _updater, _creationFee, _reimbursementFee, _minimumCampaignDuration, _maximumCampaignDuration + ); vm.assertEq(_metrom.owner(), _owner); vm.assertEq(_metrom.pendingOwner(), address(0)); vm.assertEq(_metrom.updater(), _updater); - vm.assertEq(_metrom.fee(), _fee); + vm.assertEq(_metrom.creationFee(), _creationFee); + vm.assertEq(_metrom.reimbursementFee(), _reimbursementFee); vm.assertEq(_metrom.minimumCampaignDuration(), _minimumCampaignDuration); vm.assertEq(_metrom.maximumCampaignDuration(), _maximumCampaignDuration); } @@ -71,28 +83,34 @@ contract InitializeTest is BaseTest { function testFuzz_success( address _owner, address _updater, - uint32 _fee, + uint32 _creationFee, + uint32 _reimbursementFee, uint32 _rawMinimumCampaignDuration, uint32 _maximumCampaignDuration ) public { vm.assume(_owner != address(0)); vm.assume(_updater != address(0)); - vm.assume(_fee < UNIT); + vm.assume(_creationFee < UNIT); + vm.assume(_reimbursementFee < UNIT); vm.assume(_maximumCampaignDuration > 0); uint32 _minimumCampaignDuration = uint32(bound(_rawMinimumCampaignDuration, 0, _maximumCampaignDuration - 1)); MetromHarness _metrom = MetromHarness(address(new ERC1967Proxy(address(new MetromHarness()), bytes("")))); vm.expectEmit(); - emit IMetrom.Initialize(_owner, _updater, _fee, _minimumCampaignDuration, _maximumCampaignDuration); + emit IMetrom.Initialize( + _owner, _updater, _creationFee, _reimbursementFee, _minimumCampaignDuration, _maximumCampaignDuration + ); - _metrom.initialize(_owner, _updater, _fee, _minimumCampaignDuration, _maximumCampaignDuration); + _metrom.initialize( + _owner, _updater, _creationFee, _reimbursementFee, _minimumCampaignDuration, _maximumCampaignDuration + ); vm.assertEq(_metrom.owner(), _owner); vm.assertEq(_metrom.pendingOwner(), address(0)); vm.assertEq(_metrom.updater(), _updater); - vm.assertEq(_metrom.updater(), _updater); - vm.assertEq(_metrom.fee(), _fee); + vm.assertEq(_metrom.creationFee(), _creationFee); + vm.assertEq(_metrom.reimbursementFee(), _reimbursementFee); vm.assertEq(_metrom.minimumCampaignDuration(), _minimumCampaignDuration); vm.assertEq(_metrom.maximumCampaignDuration(), _maximumCampaignDuration); } diff --git a/test/SetFee.t.sol b/test/SetCreationFee.t.sol similarity index 62% rename from test/SetFee.t.sol rename to test/SetCreationFee.t.sol index 840f90a..e4a3f29 100644 --- a/test/SetFee.t.sol +++ b/test/SetCreationFee.t.sol @@ -5,36 +5,36 @@ import {BaseTest} from "./Base.t.sol"; import {UNIT, IMetrom} from "../src/IMetrom.sol"; /// SPDX-License-Identifier: GPL-3.0-or-later -contract SetFeeTest is BaseTest { +contract SetCreationFeeTest is BaseTest { function test_failInvalid() public { vm.expectRevert(IMetrom.InvalidFee.selector); vm.prank(owner); - metrom.setFee(uint32(UNIT)); + metrom.setCreationFee(uint32(UNIT)); } function test_failForbidden() public { vm.expectRevert(IMetrom.Forbidden.selector); - metrom.setFee(10); + metrom.setCreationFee(10); } function test_success() public { - vm.assertEq(metrom.fee(), fee); + vm.assertEq(metrom.creationFee(), creationFee); uint32 _newFee = uint32(10_000); vm.prank(owner); - metrom.setFee(_newFee); + metrom.setCreationFee(_newFee); - vm.assertEq(metrom.fee(), _newFee); + vm.assertEq(metrom.creationFee(), _newFee); } function testFuzz_success(uint32 _newFee) public { vm.assume(_newFee < UNIT); - vm.assertEq(metrom.fee(), fee); + vm.assertEq(metrom.creationFee(), creationFee); vm.prank(owner); - metrom.setFee(_newFee); + metrom.setCreationFee(_newFee); - vm.assertEq(metrom.fee(), _newFee); + vm.assertEq(metrom.creationFee(), _newFee); } } diff --git a/test/SetFeeRebate.t.sol b/test/SetCreationFeeRebate.t.sol similarity index 57% rename from test/SetFeeRebate.t.sol rename to test/SetCreationFeeRebate.t.sol index 166e1a0..b7c0051 100644 --- a/test/SetFeeRebate.t.sol +++ b/test/SetCreationFeeRebate.t.sol @@ -5,54 +5,54 @@ import {BaseTest} from "./Base.t.sol"; import {UNIT, IMetrom} from "../src/IMetrom.sol"; /// SPDX-License-Identifier: GPL-3.0-or-later -contract SetFeeRebate is BaseTest { +contract SetCreationFeeRebate is BaseTest { function test_failZeroAddressAccount() public { vm.expectRevert(IMetrom.ZeroAddressAccount.selector); - metrom.setFeeRebate(address(0), uint32(UNIT - 1)); + metrom.setCreationFeeRebate(address(0), uint32(UNIT - 1)); } function test_failRebateTooHigh() public { vm.expectRevert(IMetrom.RebateTooHigh.selector); - metrom.setFeeRebate(address(1), uint32(UNIT + 1)); + metrom.setCreationFeeRebate(address(1), uint32(UNIT + 1)); } function test_failForbidden() public { vm.expectRevert(IMetrom.Forbidden.selector); - metrom.setFeeRebate(address(1), 10); + metrom.setCreationFeeRebate(address(1), 10); } function test_success() public { address _account = address(1); - vm.assertEq(metrom.feeRebate(_account), 0); + vm.assertEq(metrom.creationFeeRebate(_account), 0); uint32 _newFeeRebate = uint32(10_000); vm.prank(owner); - metrom.setFeeRebate(_account, _newFeeRebate); + metrom.setCreationFeeRebate(_account, _newFeeRebate); - vm.assertEq(metrom.feeRebate(_account), _newFeeRebate); + vm.assertEq(metrom.creationFeeRebate(_account), _newFeeRebate); } function test_successNone() public { address _account = address(1); - vm.assertEq(metrom.feeRebate(_account), 0); + vm.assertEq(metrom.creationFeeRebate(_account), 0); vm.prank(owner); - metrom.setFeeRebate(_account, uint32(0)); + metrom.setCreationFeeRebate(_account, uint32(0)); - vm.assertEq(metrom.feeRebate(_account), 0); + vm.assertEq(metrom.creationFeeRebate(_account), 0); } function testFuzz_success(address _account, uint32 _rawFeeRebate) public { vm.assume(_account != address(0)); uint32 _newFeeRebate = uint32(bound(_rawFeeRebate, 0, UNIT)); - vm.assertEq(metrom.feeRebate(_account), 0); + vm.assertEq(metrom.creationFeeRebate(_account), 0); vm.prank(owner); - metrom.setFeeRebate(_account, _newFeeRebate); + metrom.setCreationFeeRebate(_account, _newFeeRebate); - vm.assertEq(metrom.feeRebate(_account), _newFeeRebate); + vm.assertEq(metrom.creationFeeRebate(_account), _newFeeRebate); } } diff --git a/test/SetReimbursementFee.t.sol b/test/SetReimbursementFee.t.sol new file mode 100644 index 0000000..ff4f3de --- /dev/null +++ b/test/SetReimbursementFee.t.sol @@ -0,0 +1,40 @@ +pragma solidity 0.8.28; + +import {MetromHarness} from "./harnesses/MetromHarness.sol"; +import {BaseTest} from "./Base.t.sol"; +import {UNIT, IMetrom} from "../src/IMetrom.sol"; + +/// SPDX-License-Identifier: GPL-3.0-or-later +contract SetReimbursementFeeTest is BaseTest { + function test_failInvalid() public { + vm.expectRevert(IMetrom.InvalidFee.selector); + vm.prank(owner); + metrom.setReimbursementFee(uint32(UNIT)); + } + + function test_failForbidden() public { + vm.expectRevert(IMetrom.Forbidden.selector); + metrom.setReimbursementFee(10); + } + + function test_success() public { + vm.assertEq(metrom.reimbursementFee(), reimbursementFee); + + uint32 _newFee = uint32(10_000); + vm.prank(owner); + metrom.setReimbursementFee(_newFee); + + vm.assertEq(metrom.reimbursementFee(), _newFee); + } + + function testFuzz_success(uint32 _newFee) public { + vm.assume(_newFee < UNIT); + + vm.assertEq(metrom.reimbursementFee(), reimbursementFee); + + vm.prank(owner); + metrom.setReimbursementFee(_newFee); + + vm.assertEq(metrom.reimbursementFee(), _newFee); + } +} diff --git a/test/SetReimbursementFeeRebate.t.sol b/test/SetReimbursementFeeRebate.t.sol new file mode 100644 index 0000000..3930b74 --- /dev/null +++ b/test/SetReimbursementFeeRebate.t.sol @@ -0,0 +1,58 @@ +pragma solidity 0.8.28; + +import {MetromHarness} from "./harnesses/MetromHarness.sol"; +import {BaseTest} from "./Base.t.sol"; +import {UNIT, IMetrom} from "../src/IMetrom.sol"; + +/// SPDX-License-Identifier: GPL-3.0-or-later +contract SetReimbursementFeeRebate is BaseTest { + function test_failZeroAddressAccount() public { + vm.expectRevert(IMetrom.ZeroAddressAccount.selector); + metrom.setReimbursementFeeRebate(address(0), uint32(UNIT - 1)); + } + + function test_failRebateTooHigh() public { + vm.expectRevert(IMetrom.RebateTooHigh.selector); + metrom.setReimbursementFeeRebate(address(1), uint32(UNIT + 1)); + } + + function test_failForbidden() public { + vm.expectRevert(IMetrom.Forbidden.selector); + metrom.setReimbursementFeeRebate(address(1), 10); + } + + function test_success() public { + address _account = address(1); + + vm.assertEq(metrom.reimbursementFeeRebate(_account), 0); + + uint32 _newFeeRebate = uint32(10_000); + vm.prank(owner); + metrom.setReimbursementFeeRebate(_account, _newFeeRebate); + + vm.assertEq(metrom.reimbursementFeeRebate(_account), _newFeeRebate); + } + + function test_successNone() public { + address _account = address(1); + + vm.assertEq(metrom.reimbursementFeeRebate(_account), 0); + + vm.prank(owner); + metrom.setReimbursementFeeRebate(_account, uint32(0)); + + vm.assertEq(metrom.reimbursementFeeRebate(_account), 0); + } + + function testFuzz_success(address _account, uint32 _rawFeeRebate) public { + vm.assume(_account != address(0)); + uint32 _newFeeRebate = uint32(bound(_rawFeeRebate, 0, UNIT)); + + vm.assertEq(metrom.reimbursementFeeRebate(_account), 0); + + vm.prank(owner); + metrom.setReimbursementFeeRebate(_account, _newFeeRebate); + + vm.assertEq(metrom.reimbursementFeeRebate(_account), _newFeeRebate); + } +} diff --git a/test/Upgrade.t.sol b/test/Upgrade.t.sol index 55ce457..f6a3040 100644 --- a/test/Upgrade.t.sol +++ b/test/Upgrade.t.sol @@ -39,7 +39,7 @@ contract UpgradeTest is BaseTest { vm.assertEq(MetromHarnessUpgraded(address(metrom)).upgraded(), true); vm.expectRevert(Initializable.InvalidInitialization.selector); - metrom.initialize(address(1), address(1), 10, 10, 11); + metrom.initialize(address(1), address(1), 10, 10, 10, 11); } function test_successReinitialize() public { From 7771d9e575dd183249a8401f87a9fb6a5e33bb00 Mon Sep 17 00:00:00 2001 From: luzzif Date: Fri, 9 May 2025 13:41:46 +0100 Subject: [PATCH 2/3] chore: upgrade solidity to 0.30.0 --- foundry.toml | 4 ++-- script/DeployFull.s.sol | 2 +- script/DeployImplementation.s.sol | 2 +- src/Metrom.sol | 2 +- src/libraries/BaseCampaignsUtils.sol | 2 +- src/libraries/PointsCampaignsV1Utils.sol | 2 +- src/libraries/PointsCampaignsV2Utils.sol | 2 +- src/libraries/RewardsCampaignsV1Utils.sol | 2 +- src/libraries/RewardsCampaignsV2Utils.sol | 2 +- test/AcceptCampaignOwnership.t.sol | 2 +- test/AcceptOwnership.t.sol | 2 +- test/Base.t.sol | 2 +- test/CampaignReward.sol | 2 +- test/ClaimFees.sol | 2 +- test/ClaimRecoverRewards.sol | 2 +- test/ClaimRewards.sol | 2 +- test/ClaimRewardsFork.sol | 2 +- test/ClaimedCampaignReward.sol | 2 +- test/CreatePointsCampaign.sol | 2 +- test/CreateRewardsCampaign.sol | 2 +- test/DistributeRewards.sol | 2 +- test/Initialize.t.sol | 2 +- test/Ossify.t.sol | 2 +- test/SetCreationFee.t.sol | 2 +- test/SetCreationFeeRebate.t.sol | 2 +- test/SetMaximumCampaignDuration.sol | 2 +- test/SetMinimumCampaignDuration.sol | 2 +- test/SetMinimumTokenRatesTest.sol | 2 +- test/SetReimbursementFee.t.sol | 2 +- test/SetReimbursementFeeRebate.t.sol | 2 +- test/SetUpdater.t.sol | 2 +- test/TransferCampaignOwnership.t.sol | 2 +- test/TransferOwnership.t.sol | 2 +- test/Upgrade.t.sol | 2 +- test/dependencies/MintableERC20.sol | 2 +- test/dependencies/MintableFeeOnTransferERC20.sol | 2 +- test/harnesses/MetromHarness.sol | 2 +- 37 files changed, 38 insertions(+), 38 deletions(-) diff --git a/foundry.toml b/foundry.toml index 29d1a02..508bfb5 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ remappings = [ 'oz-up/=lib/openzeppelin-contracts-upgradeable/contracts/', ] optimizer = false -solc_version = "0.8.28" +solc_version = "0.8.30" [profile.production] optimizer = true @@ -17,7 +17,7 @@ evm_version = "paris" runs = 16384 [profile.ci.fuzz] -runs = 100000 +runs = 10000 [etherscan] holesky = { chain = 17000, key = "${ETHERSCAN_API_KEY}", url = "https://api-holesky.etherscan.io/api" } diff --git a/script/DeployFull.s.sol b/script/DeployFull.s.sol index c757a26..73938ed 100644 --- a/script/DeployFull.s.sol +++ b/script/DeployFull.s.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; diff --git a/script/DeployImplementation.s.sol b/script/DeployImplementation.s.sol index a1bc78b..d0a1c51 100644 --- a/script/DeployImplementation.s.sol +++ b/script/DeployImplementation.s.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {Script} from "forge-std/Script.sol"; import {console2} from "forge-std/console2.sol"; diff --git a/src/Metrom.sol b/src/Metrom.sol index e0dc012..6eff0e6 100644 --- a/src/Metrom.sol +++ b/src/Metrom.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {IERC20} from "oz/token/ERC20/IERC20.sol"; import {SafeERC20} from "oz/token/ERC20/utils/SafeERC20.sol"; diff --git a/src/libraries/BaseCampaignsUtils.sol b/src/libraries/BaseCampaignsUtils.sol index 1388381..afd135c 100644 --- a/src/libraries/BaseCampaignsUtils.sol +++ b/src/libraries/BaseCampaignsUtils.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {IMetrom} from "../IMetrom.sol"; diff --git a/src/libraries/PointsCampaignsV1Utils.sol b/src/libraries/PointsCampaignsV1Utils.sol index ce317c1..173a067 100644 --- a/src/libraries/PointsCampaignsV1Utils.sol +++ b/src/libraries/PointsCampaignsV1Utils.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {IMetrom, PointsCampaignV1, ReadonlyPointsCampaign} from "../IMetrom.sol"; diff --git a/src/libraries/PointsCampaignsV2Utils.sol b/src/libraries/PointsCampaignsV2Utils.sol index e5c74bb..3cfdb78 100644 --- a/src/libraries/PointsCampaignsV2Utils.sol +++ b/src/libraries/PointsCampaignsV2Utils.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {IMetrom, PointsCampaignV2, CreatePointsCampaignBundle, ReadonlyPointsCampaign} from "../IMetrom.sol"; diff --git a/src/libraries/RewardsCampaignsV1Utils.sol b/src/libraries/RewardsCampaignsV1Utils.sol index 46f3b4c..a0f2575 100644 --- a/src/libraries/RewardsCampaignsV1Utils.sol +++ b/src/libraries/RewardsCampaignsV1Utils.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {BaseCampaignsUtils} from "./BaseCampaignsUtils.sol"; import {IMetrom, RewardsCampaignV1, ReadonlyRewardsCampaign, Reward} from "../IMetrom.sol"; diff --git a/src/libraries/RewardsCampaignsV2Utils.sol b/src/libraries/RewardsCampaignsV2Utils.sol index 1a3def4..cee158b 100644 --- a/src/libraries/RewardsCampaignsV2Utils.sol +++ b/src/libraries/RewardsCampaignsV2Utils.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import { IMetrom, RewardsCampaignV2, CreateRewardsCampaignBundle, ReadonlyRewardsCampaign, Reward diff --git a/test/AcceptCampaignOwnership.t.sol b/test/AcceptCampaignOwnership.t.sol index 0de1422..66e4dd0 100644 --- a/test/AcceptCampaignOwnership.t.sol +++ b/test/AcceptCampaignOwnership.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/AcceptOwnership.t.sol b/test/AcceptOwnership.t.sol index ed12d26..9d82e13 100644 --- a/test/AcceptOwnership.t.sol +++ b/test/AcceptOwnership.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/Base.t.sol b/test/Base.t.sol index b7656bd..3bd7f2e 100644 --- a/test/Base.t.sol +++ b/test/Base.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {Test} from "forge-std/Test.sol"; import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; diff --git a/test/CampaignReward.sol b/test/CampaignReward.sol index c02939d..a0e0827 100644 --- a/test/CampaignReward.sol +++ b/test/CampaignReward.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/ClaimFees.sol b/test/ClaimFees.sol index 1b2a86f..51d374f 100644 --- a/test/ClaimFees.sol +++ b/test/ClaimFees.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/ClaimRecoverRewards.sol b/test/ClaimRecoverRewards.sol index f597359..d3df4f8 100644 --- a/test/ClaimRecoverRewards.sol +++ b/test/ClaimRecoverRewards.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/ClaimRewards.sol b/test/ClaimRewards.sol index 64e6c96..9608b47 100644 --- a/test/ClaimRewards.sol +++ b/test/ClaimRewards.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/ClaimRewardsFork.sol b/test/ClaimRewardsFork.sol index e71e34c..bc24352 100644 --- a/test/ClaimRewardsFork.sol +++ b/test/ClaimRewardsFork.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {Metrom} from "../src/Metrom.sol"; import {Test} from "forge-std/Test.sol"; diff --git a/test/ClaimedCampaignReward.sol b/test/ClaimedCampaignReward.sol index fd71e3d..9a2705d 100644 --- a/test/ClaimedCampaignReward.sol +++ b/test/ClaimedCampaignReward.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/CreatePointsCampaign.sol b/test/CreatePointsCampaign.sol index 92b4f12..4a05030 100644 --- a/test/CreatePointsCampaign.sol +++ b/test/CreatePointsCampaign.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/CreateRewardsCampaign.sol b/test/CreateRewardsCampaign.sol index 9fbd689..5b6a742 100644 --- a/test/CreateRewardsCampaign.sol +++ b/test/CreateRewardsCampaign.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/DistributeRewards.sol b/test/DistributeRewards.sol index 9636604..4e6ed01 100644 --- a/test/DistributeRewards.sol +++ b/test/DistributeRewards.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/Initialize.t.sol b/test/Initialize.t.sol index e108bde..6c46c6d 100644 --- a/test/Initialize.t.sol +++ b/test/Initialize.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; import {Initializable} from "oz-up/proxy/utils/Initializable.sol"; diff --git a/test/Ossify.t.sol b/test/Ossify.t.sol index 6975fe4..8457efb 100644 --- a/test/Ossify.t.sol +++ b/test/Ossify.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {ERC1967Proxy} from "oz/proxy/ERC1967/ERC1967Proxy.sol"; import {Initializable} from "oz-up/proxy/utils/Initializable.sol"; diff --git a/test/SetCreationFee.t.sol b/test/SetCreationFee.t.sol index e4a3f29..9ea2ef2 100644 --- a/test/SetCreationFee.t.sol +++ b/test/SetCreationFee.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/SetCreationFeeRebate.t.sol b/test/SetCreationFeeRebate.t.sol index b7c0051..3631430 100644 --- a/test/SetCreationFeeRebate.t.sol +++ b/test/SetCreationFeeRebate.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/SetMaximumCampaignDuration.sol b/test/SetMaximumCampaignDuration.sol index 4821afc..d87068c 100644 --- a/test/SetMaximumCampaignDuration.sol +++ b/test/SetMaximumCampaignDuration.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/SetMinimumCampaignDuration.sol b/test/SetMinimumCampaignDuration.sol index 6dfae42..d6782a5 100644 --- a/test/SetMinimumCampaignDuration.sol +++ b/test/SetMinimumCampaignDuration.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/SetMinimumTokenRatesTest.sol b/test/SetMinimumTokenRatesTest.sol index 5f5f2d9..6f08896 100644 --- a/test/SetMinimumTokenRatesTest.sol +++ b/test/SetMinimumTokenRatesTest.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/SetReimbursementFee.t.sol b/test/SetReimbursementFee.t.sol index ff4f3de..6609d37 100644 --- a/test/SetReimbursementFee.t.sol +++ b/test/SetReimbursementFee.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/SetReimbursementFeeRebate.t.sol b/test/SetReimbursementFeeRebate.t.sol index 3930b74..002db11 100644 --- a/test/SetReimbursementFeeRebate.t.sol +++ b/test/SetReimbursementFeeRebate.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/SetUpdater.t.sol b/test/SetUpdater.t.sol index 731b307..89f4ebb 100644 --- a/test/SetUpdater.t.sol +++ b/test/SetUpdater.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/TransferCampaignOwnership.t.sol b/test/TransferCampaignOwnership.t.sol index 67bbd8c..335831d 100644 --- a/test/TransferCampaignOwnership.t.sol +++ b/test/TransferCampaignOwnership.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/TransferOwnership.t.sol b/test/TransferOwnership.t.sol index 4b9f705..567a84c 100644 --- a/test/TransferOwnership.t.sol +++ b/test/TransferOwnership.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {MetromHarness} from "./harnesses/MetromHarness.sol"; import {BaseTest} from "./Base.t.sol"; diff --git a/test/Upgrade.t.sol b/test/Upgrade.t.sol index f6a3040..250f2d8 100644 --- a/test/Upgrade.t.sol +++ b/test/Upgrade.t.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {Initializable} from "oz-up/proxy/utils/Initializable.sol"; diff --git a/test/dependencies/MintableERC20.sol b/test/dependencies/MintableERC20.sol index c41f912..226dafc 100644 --- a/test/dependencies/MintableERC20.sol +++ b/test/dependencies/MintableERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {ERC20} from "oz/token/ERC20/ERC20.sol"; diff --git a/test/dependencies/MintableFeeOnTransferERC20.sol b/test/dependencies/MintableFeeOnTransferERC20.sol index 67c27f6..09815fc 100644 --- a/test/dependencies/MintableFeeOnTransferERC20.sol +++ b/test/dependencies/MintableFeeOnTransferERC20.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {ERC20} from "oz/token/ERC20/ERC20.sol"; import {UNIT} from "../../src/IMetrom.sol"; diff --git a/test/harnesses/MetromHarness.sol b/test/harnesses/MetromHarness.sol index 1ff4474..37710e8 100644 --- a/test/harnesses/MetromHarness.sol +++ b/test/harnesses/MetromHarness.sol @@ -1,4 +1,4 @@ -pragma solidity 0.8.28; +pragma solidity 0.8.30; import {RewardsCampaignsV2Utils, RewardsCampaignsV2} from "../../src/libraries/RewardsCampaignsV2Utils.sol"; import {PointsCampaignsV2Utils, PointsCampaignsV2} from "../../src/libraries/PointsCampaignsV2Utils.sol"; From 57b2894cf070101606452fe013d3db1626c25373 Mon Sep 17 00:00:00 2001 From: luzzif Date: Fri, 9 May 2025 14:51:15 +0100 Subject: [PATCH 3/3] feat: introduce helper resolved fees functions --- src/IMetrom.sol | 31 +++++++++++++++++++ src/Metrom.sol | 47 +++++++++++++++++++++++++--- test/ResolvedCreationFee.t.sol | 28 +++++++++++++++++ test/ResolvedReimbursementFee.t.sol | 48 +++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 test/ResolvedCreationFee.t.sol create mode 100644 test/ResolvedReimbursementFee.t.sol diff --git a/src/IMetrom.sol b/src/IMetrom.sol index 3fd513b..0c2216a 100644 --- a/src/IMetrom.sol +++ b/src/IMetrom.sol @@ -110,6 +110,13 @@ struct ReadonlyPointsCampaign { uint256 points; } +/// @notice Bundles data regarding a resolved fee. +struct ResolvedFee { + uint32 full; + uint32 rebate; + uint32 resolved; +} + struct RewardAmount { address token; uint256 amount; @@ -488,6 +495,14 @@ interface IMetrom { /// @return rebate The creation fee rebate for the provided account. function creationFeeRebate(address account) external view returns (uint32 rebate); + /// @notice Returns the resolved creation fee for a rewards campaign creator. + /// @param creator The creator's address. + /// @return resolvedCreationFee The resolved creation fee. + function resolvedRewardsCampaignCreationFee(address creator) + external + view + returns (ResolvedFee memory resolvedCreationFee); + /// @notice Returns the current reimbursement fee. /// @return creationFee The current reimbursement fee. function reimbursementFee() external view returns (uint32 creationFee); @@ -497,6 +512,22 @@ interface IMetrom { /// @return rebate The reimbursement fee rebate for the provided account. function reimbursementFeeRebate(address account) external view returns (uint32 rebate); + /// @notice Returns the resolved reimbursement fee for a campaign creator. + /// @param account The creator's account. + /// @return resolvedReimbursementFee The resolved reimbursement fee. + function resolvedReimbursementFeeByAddress(address account) + external + view + returns (ResolvedFee memory resolvedReimbursementFee); + + /// @notice Returns the resolved reimbursement fee for a campaign. + /// @param campaignId The campaign id. + /// @return resolvedReimbursementFee The resolved reimbursement fee. + function resolvedReimbursementFeeByCampaign(bytes32 campaignId) + external + view + returns (ResolvedFee memory resolvedReimbursementFee); + /// @notice Returns the currently enforced minimum campaign duration. /// @return minimumCampaignDuration The currently enforced minimum campaign duration. function minimumCampaignDuration() external view returns (uint32 minimumCampaignDuration); diff --git a/src/Metrom.sol b/src/Metrom.sol index 6eff0e6..f826f1e 100644 --- a/src/Metrom.sol +++ b/src/Metrom.sol @@ -20,6 +20,7 @@ import { RewardsCampaignV2, Reward, RewardAmount, + ResolvedFee, PointsCampaignV1, PointsCampaignV2, ReadonlyRewardsCampaign, @@ -140,6 +141,44 @@ contract Metrom is IMetrom, UUPSUpgradeable { if (ossified) revert Ossified(); } + function _resolvedRewardsCampaignCreationFee(address _creator) internal view returns (ResolvedFee memory) { + uint32 _fee = creationFee; + uint32 _rebate = creationFeeRebate[_creator]; + uint32 _resolved = uint32(uint64(_fee) * (UNIT - _rebate) / UNIT); + + return ResolvedFee({full: _fee, rebate: _rebate, resolved: _resolved}); + } + + function _resolvedReimbursementFeeByAddress(address _account) internal view returns (ResolvedFee memory) { + uint32 _fee = reimbursementFee; + uint32 _rebate = reimbursementFeeRebate[_account]; + uint32 _resolved = uint32(uint64(_fee) * (UNIT - _rebate) / UNIT); + + return ResolvedFee({full: _fee, rebate: _rebate, resolved: _resolved}); + } + + /// @inheritdoc IMetrom + function resolvedRewardsCampaignCreationFee(address _creator) external view override returns (ResolvedFee memory) { + return _resolvedRewardsCampaignCreationFee(_creator); + } + + /// @inheritdoc IMetrom + function resolvedReimbursementFeeByAddress(address _creator) external view override returns (ResolvedFee memory) { + return _resolvedReimbursementFeeByAddress(_creator); + } + + /// @inheritdoc IMetrom + function resolvedReimbursementFeeByCampaign(bytes32 _campaignId) + external + view + override + returns (ResolvedFee memory) + { + address _owner = rewardsCampaignsV1.get(_campaignId).owner; + if (_owner == address(0)) _owner = rewardsCampaignsV2.getExistingReadonly(_campaignId).owner; + return _resolvedReimbursementFeeByAddress(_owner); + } + /// @inheritdoc IMetrom function rewardsCampaignById(bytes32 _id) external view override returns (ReadonlyRewardsCampaign memory) { RewardsCampaignV1 storage campaignV1 = rewardsCampaignsV1.get(_id); @@ -201,16 +240,14 @@ contract Metrom is IMetrom, UUPSUpgradeable { CreateRewardsCampaignBundle[] calldata _rewardsCampaignBundles, CreatePointsCampaignBundle[] calldata _pointsCampaignBundles ) external { - uint32 _fee = creationFee; - uint32 _feeRebate = creationFeeRebate[msg.sender]; - uint32 _resolvedRewardsCampaignFee = uint32(uint64(_fee) * (UNIT - _feeRebate) / UNIT); + ResolvedFee memory _fee = _resolvedRewardsCampaignCreationFee(msg.sender); uint32 _minimumCampaignDuration = minimumCampaignDuration; uint32 _maximumCampaignDuration = maximumCampaignDuration; for (uint256 _i = 0; _i < _rewardsCampaignBundles.length; _i++) { CreateRewardsCampaignBundle calldata _rewardsCampaignBundle = _rewardsCampaignBundles[_i]; (bytes32 _id, CreatedCampaignReward[] memory _createdCampaignRewards) = createRewardsCampaign( - _rewardsCampaignBundle, _minimumCampaignDuration, _maximumCampaignDuration, _resolvedRewardsCampaignFee + _rewardsCampaignBundle, _minimumCampaignDuration, _maximumCampaignDuration, _fee.resolved ); emit CreateRewardsCampaign( _id, @@ -227,7 +264,7 @@ contract Metrom is IMetrom, UUPSUpgradeable { for (uint256 _i = 0; _i < _pointsCampaignBundles.length; _i++) { CreatePointsCampaignBundle calldata _pointsCampaignBundle = _pointsCampaignBundles[_i]; (bytes32 _id, uint256 _feeAmount) = createPointsCampaign( - _pointsCampaignBundle, _minimumCampaignDuration, _maximumCampaignDuration, _feeRebate + _pointsCampaignBundle, _minimumCampaignDuration, _maximumCampaignDuration, _fee.rebate ); emit CreatePointsCampaign( _id, diff --git a/test/ResolvedCreationFee.t.sol b/test/ResolvedCreationFee.t.sol new file mode 100644 index 0000000..30d3ca8 --- /dev/null +++ b/test/ResolvedCreationFee.t.sol @@ -0,0 +1,28 @@ +pragma solidity 0.8.30; + +import {MetromHarness} from "./harnesses/MetromHarness.sol"; +import {BaseTest} from "./Base.t.sol"; +import {UNIT, IMetrom, ResolvedFee} from "../src/IMetrom.sol"; + +/// SPDX-License-Identifier: GPL-3.0-or-later +contract ResolvedCreationFee is BaseTest { + function test_noRebate() public { + vm.prank(owner); + metrom.setCreationFee(10_000); + ResolvedFee memory _fee = metrom.resolvedRewardsCampaignCreationFee(address(this)); + vm.assertEq(_fee.full, 10_000); + vm.assertEq(_fee.rebate, 0); + vm.assertEq(_fee.resolved, 10_000); + } + + function test_rebate() public { + vm.prank(owner); + metrom.setCreationFee(10_000); + vm.prank(owner); + metrom.setCreationFeeRebate(address(this), 500_000); + ResolvedFee memory _fee = metrom.resolvedRewardsCampaignCreationFee(address(this)); + vm.assertEq(_fee.full, 10_000); + vm.assertEq(_fee.rebate, 500_000); + vm.assertEq(_fee.resolved, 5_000); + } +} diff --git a/test/ResolvedReimbursementFee.t.sol b/test/ResolvedReimbursementFee.t.sol new file mode 100644 index 0000000..4f79e1e --- /dev/null +++ b/test/ResolvedReimbursementFee.t.sol @@ -0,0 +1,48 @@ +pragma solidity 0.8.30; + +import {MetromHarness} from "./harnesses/MetromHarness.sol"; +import {BaseTest} from "./Base.t.sol"; +import {UNIT, IMetrom, ResolvedFee} from "../src/IMetrom.sol"; + +/// SPDX-License-Identifier: GPL-3.0-or-later +contract ResolvedReimbursementFee is BaseTest { + function test_noRebateByAddress() public { + vm.prank(owner); + metrom.setReimbursementFee(10_000); + ResolvedFee memory _fee = metrom.resolvedReimbursementFeeByAddress(address(this)); + vm.assertEq(_fee.full, 10_000); + vm.assertEq(_fee.rebate, 0); + vm.assertEq(_fee.resolved, 10_000); + } + + function test_rebateByAddress() public { + vm.prank(owner); + metrom.setReimbursementFee(10_000); + vm.prank(owner); + metrom.setReimbursementFeeRebate(address(this), 500_000); + ResolvedFee memory _fee = metrom.resolvedReimbursementFeeByAddress(address(this)); + vm.assertEq(_fee.full, 10_000); + vm.assertEq(_fee.rebate, 500_000); + vm.assertEq(_fee.resolved, 5_000); + } + + function test_noRebateByCampaignId() public { + vm.prank(owner); + metrom.setReimbursementFee(10_000); + ResolvedFee memory _fee = metrom.resolvedReimbursementFeeByCampaign(createFixedRewardsCampaign()); + vm.assertEq(_fee.full, 10_000); + vm.assertEq(_fee.rebate, 0); + vm.assertEq(_fee.resolved, 10_000); + } + + function test_rebateByCampaignId() public { + vm.prank(owner); + metrom.setReimbursementFee(10_000); + vm.prank(owner); + metrom.setReimbursementFeeRebate(address(this), 500_000); + ResolvedFee memory _fee = metrom.resolvedReimbursementFeeByCampaign(createFixedRewardsCampaign()); + vm.assertEq(_fee.full, 10_000); + vm.assertEq(_fee.rebate, 500_000); + vm.assertEq(_fee.resolved, 5_000); + } +}