diff --git a/src/abstracts/SablierMerkleBase.sol b/src/abstracts/SablierMerkleBase.sol index b88aa97..0945b07 100644 --- a/src/abstracts/SablierMerkleBase.sol +++ b/src/abstracts/SablierMerkleBase.sol @@ -196,7 +196,7 @@ abstract contract SablierMerkleBase is } /// @inheritdoc ISablierMerkleBase - function setMinimumFeeToZero() external override { + function lowerMinimumFee(uint256 newFee) external override { // Retrieve the factory admin. address factoryAdmin = ISablierMerkleFactoryBase(FACTORY).admin(); @@ -205,13 +205,18 @@ abstract contract SablierMerkleBase is revert Errors.SablierMerkleBase_CallerNotFactoryAdmin(factoryAdmin, msg.sender); } - uint256 previousMinimumFee = minimumFee; + uint256 previousFee = minimumFee; + + // Check: the new fee is less than the current fee. + if (newFee >= previousFee) { + revert Errors.SablierMerkleBase_NewFeeNotLower(previousFee, newFee); + } // Effect: set the minimum fee to zero. - minimumFee = 0; + minimumFee = newFee; // Log the event. - emit SetMinimumFeeToZero(factoryAdmin, previousMinimumFee); + emit LowerMinimumFee(factoryAdmin, newFee, previousFee); } /*////////////////////////////////////////////////////////////////////////// diff --git a/src/interfaces/ISablierMerkleBase.sol b/src/interfaces/ISablierMerkleBase.sol index b62f07f..11c13ef 100644 --- a/src/interfaces/ISablierMerkleBase.sol +++ b/src/interfaces/ISablierMerkleBase.sol @@ -14,8 +14,8 @@ interface ISablierMerkleBase is IAdminable { /// @notice Emitted when the admin claws back the unclaimed tokens. event Clawback(address indexed admin, address indexed to, uint128 amount); - /// @notice Emitted when the minimum fee is set to zero. - event SetMinimumFeeToZero(address indexed factoryAdmin, uint256 previousFee); + /// @notice Emitted when the minimum fee is set to a lower value. + event LowerMinimumFee(address indexed factoryAdmin, uint256 newFee, uint256 previousFee); /*////////////////////////////////////////////////////////////////////////// CONSTANT FUNCTIONS @@ -110,11 +110,12 @@ interface ISablierMerkleBase is IAdminable { /// @return feeAmount The amount of native tokens (e.g., ETH) collected as fees. function collectFees(address factoryAdmin) external returns (uint256 feeAmount); - /// @notice Sets the minimum fee required to claim the airdrop to zero. + /// @notice Sets the minimum fee to a lower value. /// - /// @dev Emits a {SetMinimumFeeToZero} event. + /// @dev Emits a {LowerMinimumFee} event. /// /// Requirements: /// - `msg.sender` must be the `FACTORY` admin. - function setMinimumFeeToZero() external; + /// - The new fee must be less than the current `minimumFee`. + function lowerMinimumFee(uint256 newFee) external; } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index b9fa0f9..3c78c22 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -30,6 +30,9 @@ library Errors { /// @notice Thrown when trying to claim with an invalid Merkle proof. error SablierMerkleBase_InvalidProof(); + /// @notice Thrown when trying to set a fee that is not lower than the current fee. + error SablierMerkleBase_NewFeeNotLower(uint256 currentFee, uint256 newFee); + /// @notice Thrown when trying to claim the same stream more than once. error SablierMerkleBase_StreamClaimed(uint256 index); diff --git a/tests/integration/concrete/campaign/instant/MerkleInstant.t.sol b/tests/integration/concrete/campaign/instant/MerkleInstant.t.sol index 4e22bbb..cc3fb0b 100644 --- a/tests/integration/concrete/campaign/instant/MerkleInstant.t.sol +++ b/tests/integration/concrete/campaign/instant/MerkleInstant.t.sol @@ -11,7 +11,7 @@ import { CollectFees_Integration_Test } from "./../shared/collect-fees/collectFe import { GetFirstClaimTime_Integration_Test } from "./../shared/get-first-claim-time/getFirstClaimTime.t.sol"; import { HasClaimed_Integration_Test } from "./../shared/has-claimed/hasClaimed.t.sol"; import { HasExpired_Integration_Test } from "./../shared/has-expired/hasExpired.t.sol"; -import { SetMinimumFeeToZero_Integration_Test } from "./../shared/set-minimum-fee-to-zero/setMinimumFeeToZero.t.sol"; +import { LowerMinimumFee_Integration_Test } from "./../shared/lower-minimum-fee/lowerMinimumFee.t.sol"; /*////////////////////////////////////////////////////////////////////////// NON-SHARED TESTS @@ -84,9 +84,9 @@ contract HasExpired_MerkleInstant_Integration_Test is } } -contract SetMinimumFeeToZero_MerkleInstant_Integration_Test is +contract LowerMinimumFee_MerkleInstant_Integration_Test is MerkleInstant_Integration_Shared_Test, - SetMinimumFeeToZero_Integration_Test + LowerMinimumFee_Integration_Test { function setUp() public override(MerkleInstant_Integration_Shared_Test, Integration_Test) { MerkleInstant_Integration_Shared_Test.setUp(); diff --git a/tests/integration/concrete/campaign/ll/MerkleLL.t.sol b/tests/integration/concrete/campaign/ll/MerkleLL.t.sol index 99e6050..34a4b94 100644 --- a/tests/integration/concrete/campaign/ll/MerkleLL.t.sol +++ b/tests/integration/concrete/campaign/ll/MerkleLL.t.sol @@ -11,7 +11,7 @@ import { CollectFees_Integration_Test } from "./../shared/collect-fees/collectFe import { GetFirstClaimTime_Integration_Test } from "./../shared/get-first-claim-time/getFirstClaimTime.t.sol"; import { HasClaimed_Integration_Test } from "./../shared/has-claimed/hasClaimed.t.sol"; import { HasExpired_Integration_Test } from "./../shared/has-expired/hasExpired.t.sol"; -import { SetMinimumFeeToZero_Integration_Test } from "./../shared/set-minimum-fee-to-zero/setMinimumFeeToZero.t.sol"; +import { LowerMinimumFee_Integration_Test } from "./../shared/lower-minimum-fee/lowerMinimumFee.t.sol"; /*////////////////////////////////////////////////////////////////////////// NON-SHARED TESTS @@ -75,9 +75,9 @@ contract HasExpired_MerkleLL_Integration_Test is MerkleLL_Integration_Shared_Tes } } -contract SetMinimumFeeToZero_MerkleLL_Integration_Test is +contract LowerMinimumFee_MerkleLL_Integration_Test is MerkleLL_Integration_Shared_Test, - SetMinimumFeeToZero_Integration_Test + LowerMinimumFee_Integration_Test { function setUp() public override(MerkleLL_Integration_Shared_Test, Integration_Test) { MerkleLL_Integration_Shared_Test.setUp(); diff --git a/tests/integration/concrete/campaign/lt/MerkleLT.t.sol b/tests/integration/concrete/campaign/lt/MerkleLT.t.sol index 1a5376b..935da22 100644 --- a/tests/integration/concrete/campaign/lt/MerkleLT.t.sol +++ b/tests/integration/concrete/campaign/lt/MerkleLT.t.sol @@ -11,7 +11,7 @@ import { CollectFees_Integration_Test } from "./../shared/collect-fees/collectFe import { GetFirstClaimTime_Integration_Test } from "./../shared/get-first-claim-time/getFirstClaimTime.t.sol"; import { HasClaimed_Integration_Test } from "./../shared/has-claimed/hasClaimed.t.sol"; import { HasExpired_Integration_Test } from "./../shared/has-expired/hasExpired.t.sol"; -import { SetMinimumFeeToZero_Integration_Test } from "./../shared/set-minimum-fee-to-zero/setMinimumFeeToZero.t.sol"; +import { LowerMinimumFee_Integration_Test } from "./../shared/lower-minimum-fee/lowerMinimumFee.t.sol"; /*////////////////////////////////////////////////////////////////////////// NON-SHARED TESTS @@ -75,9 +75,9 @@ contract HasExpired_MerkleLT_Integration_Test is MerkleLT_Integration_Shared_Tes } } -contract SetMinimumFeeToZero_MerkleLT_Integration_Test is +contract LowerMinimumFee_MerkleLT_Integration_Test is MerkleLT_Integration_Shared_Test, - SetMinimumFeeToZero_Integration_Test + LowerMinimumFee_Integration_Test { function setUp() public override(MerkleLT_Integration_Shared_Test, Integration_Test) { MerkleLT_Integration_Shared_Test.setUp(); diff --git a/tests/integration/concrete/campaign/shared/calculate-minimum-fee-in-wei/calculateMinimumFeeInWei.t.sol b/tests/integration/concrete/campaign/shared/calculate-minimum-fee-in-wei/calculateMinimumFeeInWei.t.sol index 9d5f659..fc9a0ab 100644 --- a/tests/integration/concrete/campaign/shared/calculate-minimum-fee-in-wei/calculateMinimumFeeInWei.t.sol +++ b/tests/integration/concrete/campaign/shared/calculate-minimum-fee-in-wei/calculateMinimumFeeInWei.t.sol @@ -33,7 +33,7 @@ abstract contract CalculateMinimumFeeInWei_Integration_Test is Integration_Test function test_GivenMinimumFeeZero() external givenPriceFeedAddressNotZero { resetPrank(users.admin); - merkleBase.setMinimumFeeToZero(); + merkleBase.lowerMinimumFee(0); assertEq(merkleBase.calculateMinimumFeeInWei(), 0, "minimum fee in wei"); } diff --git a/tests/integration/concrete/campaign/shared/lower-minimum-fee/lowerMinimumFee.t.sol b/tests/integration/concrete/campaign/shared/lower-minimum-fee/lowerMinimumFee.t.sol new file mode 100644 index 0000000..0319200 --- /dev/null +++ b/tests/integration/concrete/campaign/shared/lower-minimum-fee/lowerMinimumFee.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierMerkleBase } from "src/interfaces/ISablierMerkleBase.sol"; +import { Errors } from "src/libraries/Errors.sol"; + +import { Integration_Test } from "../../../../Integration.t.sol"; + +abstract contract LowerMinimumFee_Integration_Test is Integration_Test { + function test_RevertWhen_CallerNotFactoryAdmin() external { + resetPrank({ msgSender: users.eve }); + vm.expectRevert( + abi.encodeWithSelector(Errors.SablierMerkleBase_CallerNotFactoryAdmin.selector, users.admin, users.eve) + ); + merkleBase.lowerMinimumFee(MINIMUM_FEE - 1); + } + + function test_RevertWhen_NewFeeNotLower() external whenCallerFactoryAdmin { + uint256 newFee = MINIMUM_FEE + 1; + resetPrank(users.admin); + vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleBase_NewFeeNotLower.selector, MINIMUM_FEE, newFee)); + merkleBase.lowerMinimumFee(newFee); + } + + function test_WhenNewFeeNotZero() external whenCallerFactoryAdmin whenNewFeeLower { + uint256 newFee = MINIMUM_FEE - 1; + resetPrank(users.admin); + vm.expectEmit({ emitter: address(merkleBase) }); + emit ISablierMerkleBase.LowerMinimumFee(users.admin, newFee, MINIMUM_FEE); + merkleBase.lowerMinimumFee(newFee); + assertEq(merkleBase.minimumFee(), newFee); + } + + function test_WhenNewFeeZero() external whenCallerFactoryAdmin whenNewFeeLower { + uint256 newFee = 0; + resetPrank(users.admin); + vm.expectEmit({ emitter: address(merkleBase) }); + emit ISablierMerkleBase.LowerMinimumFee(users.admin, newFee, MINIMUM_FEE); + merkleBase.lowerMinimumFee(newFee); + assertEq(merkleBase.minimumFee(), newFee); + } +} diff --git a/tests/integration/concrete/campaign/shared/lower-minimum-fee/lowerMinimumFee.tree b/tests/integration/concrete/campaign/shared/lower-minimum-fee/lowerMinimumFee.tree new file mode 100644 index 0000000..cd175ad --- /dev/null +++ b/tests/integration/concrete/campaign/shared/lower-minimum-fee/lowerMinimumFee.tree @@ -0,0 +1,13 @@ +LowerMinimumFee_Integration_Test +├── when caller not factory admin +│ └── it should revert +└── when caller factory admin + ├── when new fee not lower + │ └── it should revert + └── when new fee lower + ├── when new fee not zero + │ ├── it should set minimum fee to new fee + │ └── it should emit event LowerMinimumFee + └── when new fee zero + ├── it should set minimum fee to zero + └── it should emit event LowerMinimumFee \ No newline at end of file diff --git a/tests/integration/concrete/campaign/shared/set-minimum-fee-to-zero/setMinimumFeeToZero.t.sol b/tests/integration/concrete/campaign/shared/set-minimum-fee-to-zero/setMinimumFeeToZero.t.sol deleted file mode 100644 index a85e8c1..0000000 --- a/tests/integration/concrete/campaign/shared/set-minimum-fee-to-zero/setMinimumFeeToZero.t.sol +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { ISablierMerkleBase } from "src/interfaces/ISablierMerkleBase.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { Integration_Test } from "../../../../Integration.t.sol"; - -abstract contract SetMinimumFeeToZero_Integration_Test is Integration_Test { - function test_RevertWhen_CallerNotFactoryAdmin() external { - resetPrank({ msgSender: users.eve }); - vm.expectRevert( - abi.encodeWithSelector(Errors.SablierMerkleBase_CallerNotFactoryAdmin.selector, users.admin, users.eve) - ); - merkleBase.setMinimumFeeToZero(); - } - - modifier whenCallerFactoryAdmin() { - _; - } - - function test_GivenMinimumFeeAlreadyZero() external whenCallerFactoryAdmin { - resetPrank(users.admin); - merkleBase.setMinimumFeeToZero(); - assertEq(merkleBase.minimumFee(), 0); - merkleBase.setMinimumFeeToZero(); - assertEq(merkleBase.minimumFee(), 0); - } - - function test_GivenMinimumFeeNotZero() external whenCallerFactoryAdmin { - resetPrank(users.admin); - vm.expectEmit({ emitter: address(merkleBase) }); - emit ISablierMerkleBase.SetMinimumFeeToZero(users.admin, MINIMUM_FEE); - merkleBase.setMinimumFeeToZero(); - assertEq(merkleBase.minimumFee(), 0); - } -} diff --git a/tests/integration/concrete/campaign/shared/set-minimum-fee-to-zero/setMinimumFeeToZero.tree b/tests/integration/concrete/campaign/shared/set-minimum-fee-to-zero/setMinimumFeeToZero.tree deleted file mode 100644 index 52576bf..0000000 --- a/tests/integration/concrete/campaign/shared/set-minimum-fee-to-zero/setMinimumFeeToZero.tree +++ /dev/null @@ -1,9 +0,0 @@ -SetMinimumFeeToZero_Integration_Test -├── when caller not factory admin -│ └── it should revert -└── when caller factory admin - ├── given minimum fee already zero - │ └── it should do nothing - └── given minimum fee not zero - ├── it should set minimum fee to zero - └── it should emit event SetMinimumFeeToZero \ No newline at end of file diff --git a/tests/integration/concrete/campaign/vca/MerkleVCA.t.sol b/tests/integration/concrete/campaign/vca/MerkleVCA.t.sol index 642b1b1..de03782 100644 --- a/tests/integration/concrete/campaign/vca/MerkleVCA.t.sol +++ b/tests/integration/concrete/campaign/vca/MerkleVCA.t.sol @@ -11,7 +11,7 @@ import { CollectFees_Integration_Test } from "./../shared/collect-fees/collectFe import { GetFirstClaimTime_Integration_Test } from "./../shared/get-first-claim-time/getFirstClaimTime.t.sol"; import { HasClaimed_Integration_Test } from "./../shared/has-claimed/hasClaimed.t.sol"; import { HasExpired_Integration_Test } from "./../shared/has-expired/hasExpired.t.sol"; -import { SetMinimumFeeToZero_Integration_Test } from "./../shared/set-minimum-fee-to-zero/setMinimumFeeToZero.t.sol"; +import { LowerMinimumFee_Integration_Test } from "./../shared/lower-minimum-fee/lowerMinimumFee.t.sol"; /*////////////////////////////////////////////////////////////////////////// NON-SHARED TESTS @@ -75,9 +75,9 @@ contract HasExpired_MerkleVCA_Integration_Test is MerkleVCA_Integration_Shared_T } } -contract SetMinimumFeeToZero_MerkleVCA_Integration_Test is +contract LowerMinimumFee_MerkleVCA_Integration_Test is MerkleVCA_Integration_Shared_Test, - SetMinimumFeeToZero_Integration_Test + LowerMinimumFee_Integration_Test { function setUp() public override(MerkleVCA_Integration_Shared_Test, Integration_Test) { MerkleVCA_Integration_Shared_Test.setUp(); diff --git a/tests/utils/Modifiers.sol b/tests/utils/Modifiers.sol index 13a5c6e..00b266b 100644 --- a/tests/utils/Modifiers.sol +++ b/tests/utils/Modifiers.sol @@ -19,6 +19,10 @@ abstract contract Modifiers is EvmUtilsBase { GIVEN //////////////////////////////////////////////////////////////////////////*/ + modifier whenCallerFactoryAdmin() { + _; + } + modifier givenCampaignNotExists() { _; } @@ -95,6 +99,10 @@ abstract contract Modifiers is EvmUtilsBase { _; } + modifier whenNewFeeLower() { + _; + } + modifier whenNewFeeDoesNotExceedTheMaximumFee() { _; }