diff --git a/bun.lockb b/bun.lockb index eaad830..fb69832 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/remappings.txt b/remappings.txt index 752b23c..c949669 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,6 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/math/=node_modules/@prb/math/ -@sablier/lockup/=node_modules/@sablier/lockup/src/core/ +@sablier/lockup/=node_modules/@sablier/lockup/src/ @sablier/lockup-precompiles=node_modules/@sablier/lockup/precompiles/ forge-std/=node_modules/forge-std/ solady/=node_modules/solady/ diff --git a/src/SablierMerkleFactory.sol b/src/SablierMerkleFactory.sol index d2d5299..526c1f4 100644 --- a/src/SablierMerkleFactory.sol +++ b/src/SablierMerkleFactory.sol @@ -11,7 +11,6 @@ import { ISablierMerkleFactory } from "./interfaces/ISablierMerkleFactory.sol"; import { ISablierMerkleInstant } from "./interfaces/ISablierMerkleInstant.sol"; import { ISablierMerkleLL } from "./interfaces/ISablierMerkleLL.sol"; import { ISablierMerkleLT } from "./interfaces/ISablierMerkleLT.sol"; -import { Errors } from "./libraries/Errors.sol"; import { SablierMerkleInstant } from "./SablierMerkleInstant.sol"; import { SablierMerkleLL } from "./SablierMerkleLL.sol"; import { SablierMerkleLT } from "./SablierMerkleLT.sol"; @@ -64,59 +63,18 @@ contract SablierMerkleFactory is } /*////////////////////////////////////////////////////////////////////////// - ADMIN-FACING NON-CONSTANT FUNCTIONS + USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ISablierMerkleFactory - function resetCustomFee(address campaignCreator) external override onlyAdmin { - delete _customFees[campaignCreator]; - - // Log the reset. - emit ResetCustomFee({ admin: msg.sender, campaignCreator: campaignCreator }); - } - - /// @inheritdoc ISablierMerkleFactory - function setCustomFee(address campaignCreator, uint256 newFee) external override onlyAdmin { - MerkleFactory.CustomFee storage customFeeByUser = _customFees[campaignCreator]; - - // Check: if the user is not in the custom fee list. - if (!customFeeByUser.enabled) { - customFeeByUser.enabled = true; - } - - // Effect: update the custom fee for the given campaign creator. - customFeeByUser.fee = newFee; - - // Log the update. - emit SetCustomFee({ admin: msg.sender, campaignCreator: campaignCreator, customFee: newFee }); - } - - /// @inheritdoc ISablierMerkleFactory - function setDefaultFee(uint256 defaultFee_) external override onlyAdmin { - // Effect: update the default fee. - defaultFee = defaultFee_; + function collectFees(ISablierMerkleBase merkleBase) external override { + // Effect: collect the fees from the MerkleBase contract. + uint256 feeAmount = merkleBase.collectFees(admin); - emit SetDefaultFee(msg.sender, defaultFee_); + // Log the fee withdrawal. + emit CollectFees({ admin: admin, merkleBase: merkleBase, feeAmount: feeAmount }); } - /// @inheritdoc ISablierMerkleFactory - function withdrawFees(address payable to, ISablierMerkleBase merkleBase) external override onlyAdmin { - // Check: the withdrawal address is not zero. - if (to == address(0)) { - revert Errors.SablierMerkleFactory_WithdrawToZeroAddress(); - } - - // Effect: call `withdrawFees` on the MerkleBase contract. - uint256 fees = merkleBase.withdrawFees(to); - - // Log the withdrawal. - emit WithdrawFees({ admin: msg.sender, merkleBase: merkleBase, to: to, fees: fees }); - } - - /*////////////////////////////////////////////////////////////////////////// - USER-FACING NON-CONSTANT FUNCTIONS - //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc ISablierMerkleFactory function createMerkleInstant( MerkleBase.ConstructorParams memory baseParams, @@ -241,6 +199,38 @@ contract SablierMerkleFactory is ); } + /// @inheritdoc ISablierMerkleFactory + function resetCustomFee(address campaignCreator) external override onlyAdmin { + delete _customFees[campaignCreator]; + + // Log the reset. + emit ResetCustomFee({ admin: msg.sender, campaignCreator: campaignCreator }); + } + + /// @inheritdoc ISablierMerkleFactory + function setCustomFee(address campaignCreator, uint256 newFee) external override onlyAdmin { + MerkleFactory.CustomFee storage customFeeByUser = _customFees[campaignCreator]; + + // Check: if the user is not in the custom fee list. + if (!customFeeByUser.enabled) { + customFeeByUser.enabled = true; + } + + // Effect: update the custom fee for the given campaign creator. + customFeeByUser.fee = newFee; + + // Log the update. + emit SetCustomFee({ admin: msg.sender, campaignCreator: campaignCreator, customFee: newFee }); + } + + /// @inheritdoc ISablierMerkleFactory + function setDefaultFee(uint256 defaultFee_) external override onlyAdmin { + // Effect: update the default fee. + defaultFee = defaultFee_; + + emit SetDefaultFee(msg.sender, defaultFee_); + } + /*////////////////////////////////////////////////////////////////////////// PRIVATE NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ diff --git a/src/abstracts/SablierMerkleBase.sol b/src/abstracts/SablierMerkleBase.sol index 1ad3392..b843382 100644 --- a/src/abstracts/SablierMerkleBase.sol +++ b/src/abstracts/SablierMerkleBase.sol @@ -165,20 +165,20 @@ abstract contract SablierMerkleBase is } /// @inheritdoc ISablierMerkleBase - function withdrawFees(address payable to) external override returns (uint256 feeAmount) { - // Check: the msg.sender is the FACTORY. + function collectFees(address factoryAdmin) external override returns (uint256 feeAmount) { + // Check: the caller is the FACTORY. if (msg.sender != FACTORY) { revert Errors.SablierMerkleBase_CallerNotFactory(FACTORY, msg.sender); } feeAmount = address(this).balance; - // Effect: transfer the fees to the provided address. - (bool success,) = to.call{ value: feeAmount }(""); + // Effect: transfer the fees to the factory admin. + (bool success,) = factoryAdmin.call{ value: feeAmount }(""); // Revert if the call failed. if (!success) { - revert Errors.SablierMerkleBase_FeeWithdrawFailed(to, feeAmount); + revert Errors.SablierMerkleBase_FeeTransferFail(factoryAdmin, feeAmount); } } diff --git a/src/interfaces/ISablierMerkleBase.sol b/src/interfaces/ISablierMerkleBase.sol index 4bf145b..a92bd1e 100644 --- a/src/interfaces/ISablierMerkleBase.sol +++ b/src/interfaces/ISablierMerkleBase.sol @@ -88,15 +88,12 @@ interface ISablierMerkleBase is IAdminable { /// @param amount The amount of tokens to claw back. function clawback(address to, uint128 amount) external; - /// @notice Withdraws the accrued fees to the provided address. - /// - /// @dev This function transfers ETH to the provided address. If the receiver is a contract, it must be able to - /// receive ETH. + /// @notice Collects the accrued fees by transferring them to `FACTORY` admin. /// /// Requirements: /// - msg.sender must be the `FACTORY` contract. /// - /// @param to The address to transfer the fees to. - /// @return feeAmount The amount of ETH transferred to the provided address. - function withdrawFees(address payable to) external returns (uint256 feeAmount); + /// @param factoryAdmin The address of the `FACTORY` admin. + /// @return feeAmount The amount of ETH withdrawn. + function collectFees(address factoryAdmin) external returns (uint256 feeAmount); } diff --git a/src/interfaces/ISablierMerkleFactory.sol b/src/interfaces/ISablierMerkleFactory.sol index 8112733..73d8a4e 100644 --- a/src/interfaces/ISablierMerkleFactory.sol +++ b/src/interfaces/ISablierMerkleFactory.sol @@ -22,6 +22,9 @@ interface ISablierMerkleFactory is IAdminable { EVENTS //////////////////////////////////////////////////////////////////////////*/ + /// @notice Emitted when the accrued fees are collected. + event CollectFees(address indexed admin, ISablierMerkleBase indexed merkleBase, uint256 feeAmount); + /// @notice Emitted when a {SablierMerkleInstant} campaign is created. event CreateMerkleInstant( ISablierMerkleInstant indexed merkleInstant, @@ -68,9 +71,6 @@ interface ISablierMerkleFactory is IAdminable { /// @notice Emitted when the default fee is set by the admin. event SetDefaultFee(address indexed admin, uint256 defaultFee); - /// @notice Emitted when the fees are claimed by the Sablier admin. - event WithdrawFees(address indexed admin, ISablierMerkleBase indexed merkleBase, address to, uint256 fees); - /*////////////////////////////////////////////////////////////////////////// CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ @@ -96,6 +96,15 @@ interface ISablierMerkleFactory is IAdminable { NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @notice Collects the fees accrued in the `merkleBase` contract, and transfers them to the factory admin. + /// @dev Emits a {CollectFees} event. + /// + /// Notes: + /// - If the admin is a contract, it must be able to receive ETH. + /// + /// @param merkleBase The address of the Merkle contract where the fees are collected from. + function collectFees(ISablierMerkleBase merkleBase) external; + /// @notice Creates a new MerkleInstant campaign for instant distribution of tokens. /// /// @dev Emits a {CreateMerkleInstant} event. @@ -214,18 +223,4 @@ interface ISablierMerkleFactory is IAdminable { /// /// @param defaultFee The new default fee to be set. function setDefaultFee(uint256 defaultFee) external; - - /// @notice Withdraws the fees accrued in the `merkleBase` contract. - /// @dev Emits a {WithdrawFees} event. - /// - /// Notes: - /// - This function transfers ETH to the provided address. If the receiver is a contract, it must be able to receive - /// ETH. - /// - /// Requirements: - /// - `msg.sender` must be the admin. - /// - `to` must not be the zero address. - /// - /// @param to The address to transfer the fees to. - function withdrawFees(address payable to, ISablierMerkleBase merkleBase) external; } diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index da8784f..99539d4 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -10,12 +10,6 @@ library Errors { error CallerNotAdmin(address admin, address caller); - /*////////////////////////////////////////////////////////////////////////// - SABLIER-MERKLE-FACTORY - //////////////////////////////////////////////////////////////////////////*/ - - error SablierMerkleFactory_WithdrawToZeroAddress(); - /*////////////////////////////////////////////////////////////////////////// SABLIER-MERKLE-BASE //////////////////////////////////////////////////////////////////////////*/ @@ -34,7 +28,7 @@ library Errors { error SablierMerkleBase_ClawbackNotAllowed(uint256 blockTimestamp, uint40 expiration, uint40 firstClaimTime); /// @notice Thrown if the fees withdrawal failed. - error SablierMerkleBase_FeeWithdrawFailed(address to, uint256 amount); + error SablierMerkleBase_FeeTransferFail(address factoryAdmin, uint256 feeAmount); /// @notice Thrown when trying to claim with an insufficient fee payment. error SablierMerkleBase_InsufficientFeePayment(uint256 feePaid, uint256 fee); diff --git a/tests/Base.t.sol b/tests/Base.t.sol index f446c59..55ea7ce 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -73,7 +73,7 @@ abstract contract Base_Test is Assertions, Constants, DeployOptimized, Modifiers deployProtocolConditionally(); // Set the default fee on the Merkle factory. - merkleFactory.setDefaultFee(defaults.DEFAULT_FEE()); + merkleFactory.setDefaultFee(defaults.FEE()); // Create users for testing. users.campaignOwner = createUser("CampaignOwner"); diff --git a/tests/fork/merkle-campaign/MerkleInstant.t.sol b/tests/fork/merkle-campaign/MerkleInstant.t.sol index bcf6405..380dc3f 100644 --- a/tests/fork/merkle-campaign/MerkleInstant.t.sol +++ b/tests/fork/merkle-campaign/MerkleInstant.t.sol @@ -99,7 +99,7 @@ abstract contract MerkleInstant_Fork_Test is Fork_Test { // Make the campaign owner as the caller. resetPrank({ msgSender: params.campaignOwner }); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); vars.expectedMerkleInstant = computeMerkleInstantAddress( params.campaignOwner, params.campaignOwner, FORK_TOKEN, vars.merkleRoot, params.expiration, fee @@ -215,22 +215,14 @@ abstract contract MerkleInstant_Fork_Test is Fork_Test { } /*////////////////////////////////////////////////////////////////////////// - WITHDRAW-FEE + COLLECT-FEES //////////////////////////////////////////////////////////////////////////*/ - // Make the factory admin as the caller. - resetPrank({ msgSender: users.admin }); - vm.expectEmit({ emitter: address(merkleFactory) }); - emit ISablierMerkleFactory.WithdrawFees({ - admin: users.admin, - merkleBase: vars.merkleInstant, - to: users.admin, - fees: fee - }); - merkleFactory.withdrawFees({ to: payable(users.admin), merkleBase: vars.merkleInstant }); + emit ISablierMerkleFactory.CollectFees({ admin: users.admin, merkleBase: vars.merkleInstant, feeAmount: fee }); + merkleFactory.collectFees({ merkleBase: vars.merkleInstant }); - assertEq(address(vars.merkleInstant).balance, 0, "merkle lockup ether balance"); - assertEq(users.admin.balance, fee, "admin ether balance"); + assertEq(address(vars.merkleInstant).balance, 0, "merkleInstant ETH balance"); + assertEq(users.admin.balance, fee, "admin ETH balance"); } } diff --git a/tests/fork/merkle-campaign/MerkleLL.t.sol b/tests/fork/merkle-campaign/MerkleLL.t.sol index 3652bf1..dae1206 100644 --- a/tests/fork/merkle-campaign/MerkleLL.t.sol +++ b/tests/fork/merkle-campaign/MerkleLL.t.sol @@ -104,7 +104,7 @@ abstract contract MerkleLL_Fork_Test is Fork_Test { // Make the campaign owner as the caller. resetPrank({ msgSender: params.campaignOwner }); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); vars.expectedLL = computeMerkleLLAddress( params.campaignOwner, params.campaignOwner, FORK_TOKEN, vars.merkleRoot, params.expiration, fee @@ -244,22 +244,14 @@ abstract contract MerkleLL_Fork_Test is Fork_Test { } /*////////////////////////////////////////////////////////////////////////// - WITHDRAW-FEE + COLLECT-FEES //////////////////////////////////////////////////////////////////////////*/ - // Make the factory admin as the caller. - resetPrank({ msgSender: users.admin }); - vm.expectEmit({ emitter: address(merkleFactory) }); - emit ISablierMerkleFactory.WithdrawFees({ - admin: users.admin, - merkleBase: vars.merkleLL, - to: users.admin, - fees: fee - }); - merkleFactory.withdrawFees({ to: payable(users.admin), merkleBase: vars.merkleLL }); + emit ISablierMerkleFactory.CollectFees({ admin: users.admin, merkleBase: vars.merkleLL, feeAmount: fee }); + merkleFactory.collectFees({ merkleBase: vars.merkleLL }); - assertEq(address(vars.merkleLL).balance, 0, "merkle lockup ether balance"); - assertEq(users.admin.balance, fee, "admin ether balance"); + assertEq(address(vars.merkleLL).balance, 0, "merkleLL ETH balance"); + assertEq(users.admin.balance, fee, "admin ETH balance"); } } diff --git a/tests/fork/merkle-campaign/MerkleLT.t.sol b/tests/fork/merkle-campaign/MerkleLT.t.sol index 9b5245a..efc8f4d 100644 --- a/tests/fork/merkle-campaign/MerkleLT.t.sol +++ b/tests/fork/merkle-campaign/MerkleLT.t.sol @@ -100,7 +100,7 @@ abstract contract MerkleLT_Fork_Test is Fork_Test { // Make the campaign owner as the caller. resetPrank({ msgSender: params.campaignOwner }); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); vars.expectedLT = computeMerkleLTAddress( params.campaignOwner, params.campaignOwner, FORK_TOKEN, vars.merkleRoot, params.expiration, fee @@ -246,22 +246,14 @@ abstract contract MerkleLT_Fork_Test is Fork_Test { } /*////////////////////////////////////////////////////////////////////////// - WITHDRAW-FEE + COLLECT-FEES //////////////////////////////////////////////////////////////////////////*/ - // Make the factory admin as the caller. - resetPrank({ msgSender: users.admin }); - vm.expectEmit({ emitter: address(merkleFactory) }); - emit ISablierMerkleFactory.WithdrawFees({ - admin: users.admin, - merkleBase: vars.merkleLT, - to: users.admin, - fees: fee - }); - merkleFactory.withdrawFees({ to: payable(users.admin), merkleBase: vars.merkleLT }); + emit ISablierMerkleFactory.CollectFees({ admin: users.admin, merkleBase: vars.merkleLT, feeAmount: fee }); + merkleFactory.collectFees({ merkleBase: vars.merkleLT }); - assertEq(address(vars.merkleLT).balance, 0, "merkle lockup ether balance"); - assertEq(users.admin.balance, fee, "admin ether balance"); + assertEq(address(vars.merkleLT).balance, 0, "merkleLT ETH balance"); + assertEq(users.admin.balance, fee, "admin ETH balance"); } } diff --git a/tests/integration/Integration.t.sol b/tests/integration/Integration.t.sol index 764b713..04d48cf 100644 --- a/tests/integration/Integration.t.sol +++ b/tests/integration/Integration.t.sol @@ -52,7 +52,7 @@ contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function claim() internal { - merkleBase.claim{ value: defaults.DEFAULT_FEE() }({ + merkleBase.claim{ value: defaults.FEE() }({ index: defaults.INDEX1(), recipient: users.recipient1, amount: defaults.CLAIM_AMOUNT(), @@ -65,7 +65,7 @@ contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function computeMerkleInstantAddress(address campaignOwner, uint40 expiration) internal view returns (address) { - return computeMerkleInstantAddress(campaignOwner, expiration, defaults.DEFAULT_FEE()); + return computeMerkleInstantAddress(campaignOwner, expiration, defaults.FEE()); } function computeMerkleInstantAddress( @@ -112,7 +112,7 @@ contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function computeMerkleLLAddress(address campaignOwner, uint40 expiration) internal view returns (address) { - return computeMerkleLLAddress(campaignOwner, expiration, defaults.DEFAULT_FEE()); + return computeMerkleLLAddress(campaignOwner, expiration, defaults.FEE()); } function computeMerkleLLAddress( @@ -163,7 +163,7 @@ contract Integration_Test is Base_Test { //////////////////////////////////////////////////////////////////////////*/ function computeMerkleLTAddress(address campaignOwner, uint40 expiration) internal view returns (address) { - return computeMerkleLTAddress(campaignOwner, expiration, defaults.DEFAULT_FEE()); + return computeMerkleLTAddress(campaignOwner, expiration, defaults.FEE()); } function computeMerkleLTAddress( diff --git a/tests/integration/concrete/factory/collect-fees/collectFees.t.sol b/tests/integration/concrete/factory/collect-fees/collectFees.t.sol new file mode 100644 index 0000000..dd95820 --- /dev/null +++ b/tests/integration/concrete/factory/collect-fees/collectFees.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierMerkleBase } from "src/interfaces/ISablierMerkleBase.sol"; +import { ISablierMerkleFactory } from "src/interfaces/ISablierMerkleFactory.sol"; +import { Errors } from "src/libraries/Errors.sol"; + +import { Integration_Test } from "../../../Integration.t.sol"; + +contract CollectFees_Integration_Test is Integration_Test { + function setUp() public virtual override { + Integration_Test.setUp(); + + // Set the `merkleBase` to the merkleLL contract to use it in the tests. + merkleBase = ISablierMerkleBase(merkleLL); + + // Claim to collect some fees. + resetPrank(users.recipient); + claim(); + } + + function test_RevertWhen_ProvidedMerkleLockupNotValid() external { + vm.expectRevert(); + merkleFactory.collectFees(ISablierMerkleBase(users.eve)); + } + + function test_WhenFactoryAdminIsNotContract() external whenProvidedMerkleLockupValid { + _test_CollectFees(users.admin); + } + + function test_RevertWhen_FactoryAdminDoesNotImplementReceiveFunction() + external + whenProvidedMerkleLockupValid + whenFactoryAdminIsContract + { + // Transfer the admin to a contract that does not implement the receive function. + resetPrank({ msgSender: users.admin }); + merkleFactory.transferAdmin(address(contractWithoutReceiveEth)); + + // Make the contract the caller. + resetPrank({ msgSender: address(contractWithoutReceiveEth) }); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierMerkleBase_FeeTransferFail.selector, + address(contractWithoutReceiveEth), + address(merkleBase).balance + ) + ); + merkleFactory.collectFees(merkleBase); + } + + function test_WhenFactoryAdminImplementsReceiveFunction() + external + whenProvidedMerkleLockupValid + whenFactoryAdminIsContract + { + // Transfer the admin to a contract that implements the receive function. + resetPrank({ msgSender: users.admin }); + merkleFactory.transferAdmin(address(contractWithReceiveEth)); + + _test_CollectFees(address(contractWithReceiveEth)); + } + + function _test_CollectFees(address admin) private { + // Load the initial ETH balance of the admin. + uint256 initialAdminBalance = admin.balance; + + // It should emit a {CollectFees} event. + vm.expectEmit({ emitter: address(merkleFactory) }); + emit ISablierMerkleFactory.CollectFees({ admin: admin, merkleBase: merkleBase, feeAmount: defaults.FEE() }); + + // Make Alice the caller. + resetPrank({ msgSender: users.eve }); + + merkleFactory.collectFees(merkleBase); + + // It should decrease merkle contract balance to zero. + assertEq(address(merkleBase).balance, 0, "merkle lockup ETH balance"); + + // It should transfer fee to the factory admin. + assertEq(admin.balance, initialAdminBalance + defaults.FEE(), "admin ETH balance"); + } +} diff --git a/tests/integration/concrete/factory/collect-fees/collectFees.tree b/tests/integration/concrete/factory/collect-fees/collectFees.tree new file mode 100644 index 0000000..9b363c0 --- /dev/null +++ b/tests/integration/concrete/factory/collect-fees/collectFees.tree @@ -0,0 +1,15 @@ +CollectFees_Integration_Test +├── when provided merkle lockup not valid +│ └── it should revert +└── when provided merkle lockup valid + ├── when factory admin is not contract + │ ├── it should transfer fee to the factory admin + │ ├── it should decrease merkle contract balance to zero + │ └── it should emit a {CollectFees} event + └── when factory admin is contract + ├── when factory admin does not implement receive function + │ └── it should revert + └── when factory admin implements receive function + ├── it should transfer fee to the factory admin + ├── it should decrease merkle contract balance to zero + └── it should emit a {CollectFees} event diff --git a/tests/integration/concrete/factory/create-merkle-instant/createMerkleInstant.t.sol b/tests/integration/concrete/factory/create-merkle-instant/createMerkleInstant.t.sol index 21a788f..c99b7b4 100644 --- a/tests/integration/concrete/factory/create-merkle-instant/createMerkleInstant.t.sol +++ b/tests/integration/concrete/factory/create-merkle-instant/createMerkleInstant.t.sol @@ -114,7 +114,7 @@ contract CreateMerkleInstant_Integration_Test is Integration_Test { baseParams: baseParams, aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT(), - fee: defaults.DEFAULT_FEE() + fee: defaults.FEE() }); ISablierMerkleInstant actualInstant = createMerkleInstant(campaignOwner, expiration); @@ -124,7 +124,7 @@ contract CreateMerkleInstant_Integration_Test is Integration_Test { ); // It should create the campaign with custom fee. - assertEq(actualInstant.FEE(), defaults.DEFAULT_FEE(), "default fee"); + assertEq(actualInstant.FEE(), defaults.FEE(), "default fee"); // It should set the current factory address. assertEq(actualInstant.FACTORY(), address(merkleFactory), "factory"); diff --git a/tests/integration/concrete/factory/create-merkle-ll/createMerkleLL.t.sol b/tests/integration/concrete/factory/create-merkle-ll/createMerkleLL.t.sol index 3938a7a..561792b 100644 --- a/tests/integration/concrete/factory/create-merkle-ll/createMerkleLL.t.sol +++ b/tests/integration/concrete/factory/create-merkle-ll/createMerkleLL.t.sol @@ -134,7 +134,7 @@ contract CreateMerkleLL_Integration_Test is Integration_Test { schedule: defaults.schedule(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT(), - fee: defaults.DEFAULT_FEE() + fee: defaults.FEE() }); ISablierMerkleLL actualLL = createMerkleLL(campaignOwner, expiration); @@ -142,7 +142,7 @@ contract CreateMerkleLL_Integration_Test is Integration_Test { assertEq(address(actualLL), expectedLL, "MerkleLL contract does not match computed address"); // It should create the campaign with custom fee. - assertEq(actualLL.FEE(), defaults.DEFAULT_FEE(), "default fee"); + assertEq(actualLL.FEE(), defaults.FEE(), "default fee"); // It should set the current factory address. assertEq(actualLL.FACTORY(), address(merkleFactory), "factory"); diff --git a/tests/integration/concrete/factory/create-merkle-lt/createMerkleLT.t.sol b/tests/integration/concrete/factory/create-merkle-lt/createMerkleLT.t.sol index abd6d8c..96f26bc 100644 --- a/tests/integration/concrete/factory/create-merkle-lt/createMerkleLT.t.sol +++ b/tests/integration/concrete/factory/create-merkle-lt/createMerkleLT.t.sol @@ -140,7 +140,7 @@ contract CreateMerkleLT_Integration_Test is Integration_Test { totalDuration: defaults.TOTAL_DURATION(), aggregateAmount: defaults.AGGREGATE_AMOUNT(), recipientCount: defaults.RECIPIENT_COUNT(), - fee: defaults.DEFAULT_FEE() + fee: defaults.FEE() }); ISablierMerkleLT actualLT = createMerkleLT(campaignOwner, expiration); @@ -148,7 +148,7 @@ contract CreateMerkleLT_Integration_Test is Integration_Test { assertEq(address(actualLT), expectedLT, "MerkleLT contract does not match computed address"); // It should create the campaign with custom fee. - assertEq(actualLT.FEE(), defaults.DEFAULT_FEE(), "default fee"); + assertEq(actualLT.FEE(), defaults.FEE(), "default fee"); // It should set the current factory address. assertEq(actualLT.FACTORY(), address(merkleFactory), "factory"); } diff --git a/tests/integration/concrete/factory/set-default-fee/setDefaultFee.t.sol b/tests/integration/concrete/factory/set-default-fee/setDefaultFee.t.sol index 962941a..ef87791 100644 --- a/tests/integration/concrete/factory/set-default-fee/setDefaultFee.t.sol +++ b/tests/integration/concrete/factory/set-default-fee/setDefaultFee.t.sol @@ -7,7 +7,7 @@ import { Integration_Test } from "./../../../Integration.t.sol"; contract SetDefaultFee_Integration_Test is Integration_Test { function test_RevertWhen_CallerNotAdmin() external { - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); resetPrank({ msgSender: users.eve }); vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); merkleFactory.setDefaultFee({ defaultFee: fee }); @@ -18,11 +18,11 @@ contract SetDefaultFee_Integration_Test is Integration_Test { // It should emit a {SetDefaultFee} event. vm.expectEmit({ emitter: address(merkleFactory) }); - emit ISablierMerkleFactory.SetDefaultFee({ admin: users.admin, defaultFee: defaults.DEFAULT_FEE() }); + emit ISablierMerkleFactory.SetDefaultFee({ admin: users.admin, defaultFee: defaults.FEE() }); - merkleFactory.setDefaultFee({ defaultFee: defaults.DEFAULT_FEE() }); + merkleFactory.setDefaultFee({ defaultFee: defaults.FEE() }); // It should set the default fee. - assertEq(merkleFactory.defaultFee(), defaults.DEFAULT_FEE(), "default fee"); + assertEq(merkleFactory.defaultFee(), defaults.FEE(), "default fee"); } } diff --git a/tests/integration/concrete/factory/withdraw-fees/withdrawFees.t.sol b/tests/integration/concrete/factory/withdraw-fees/withdrawFees.t.sol deleted file mode 100644 index 6ba6f02..0000000 --- a/tests/integration/concrete/factory/withdraw-fees/withdrawFees.t.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { ISablierMerkleBase } from "src/interfaces/ISablierMerkleBase.sol"; -import { ISablierMerkleFactory } from "src/interfaces/ISablierMerkleFactory.sol"; -import { Errors } from "src/libraries/Errors.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -contract WithdrawFees_Integration_Test is Integration_Test { - function setUp() public virtual override { - Integration_Test.setUp(); - - // Set the `merkleBase` to the merkleLL contract to use it in the tests. - merkleBase = ISablierMerkleBase(merkleLL); - - // Claim to collect some fees. - resetPrank(users.recipient); - claim(); - } - - function test_RevertWhen_CallerNotAdmin() external { - resetPrank(users.eve); - - vm.expectRevert(abi.encodeWithSelector(Errors.CallerNotAdmin.selector, users.admin, users.eve)); - merkleFactory.withdrawFees(users.eve, merkleBase); - } - - function test_RevertWhen_WithdrawalAddressZero() external whenCallerAdmin { - vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleFactory_WithdrawToZeroAddress.selector)); - merkleFactory.withdrawFees(payable(address(0)), merkleBase); - } - - function test_RevertWhen_ProvidedMerkleLockupNotValid() external whenCallerAdmin whenWithdrawalAddressNotZero { - vm.expectRevert(); - merkleFactory.withdrawFees(users.eve, ISablierMerkleBase(users.eve)); - } - - function test_WhenProvidedAddressNotContract() external whenCallerAdmin whenProvidedMerkleLockupValid { - uint256 previousToBalance = users.eve.balance; - - // It should emit {WithdrawFees} event. - vm.expectEmit({ emitter: address(merkleFactory) }); - emit ISablierMerkleFactory.WithdrawFees({ - admin: users.admin, - merkleBase: merkleBase, - to: users.eve, - fees: defaults.DEFAULT_FEE() - }); - - merkleFactory.withdrawFees(users.eve, merkleBase); - - // It should set the ETH balance to 0. - assertEq(address(merkleBase).balance, 0, "merkle lockup eth balance"); - // It should transfer fee collected in ETH to the provided address. - assertEq(users.eve.balance, previousToBalance + defaults.DEFAULT_FEE(), "eth balance"); - } - - function test_RevertWhen_ProvidedAddressNotImplementReceiveEth() - external - whenCallerAdmin - whenProvidedMerkleLockupValid - whenProvidedAddressContract - { - address payable noReceiveEth = payable(address(contractWithoutReceiveEth)); - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierMerkleBase_FeeWithdrawFailed.selector, noReceiveEth, address(merkleBase).balance - ) - ); - merkleFactory.withdrawFees(noReceiveEth, merkleBase); - } - - function test_WhenProvidedAddressImplementReceiveEth() - external - whenCallerAdmin - whenProvidedMerkleLockupValid - whenProvidedAddressContract - { - address payable receiveEth = payable(address(contractWithReceiveEth)); - - // It should emit {WithdrawFees} event. - vm.expectEmit({ emitter: address(merkleFactory) }); - emit ISablierMerkleFactory.WithdrawFees({ - admin: users.admin, - merkleBase: merkleBase, - to: receiveEth, - fees: defaults.DEFAULT_FEE() - }); - - merkleFactory.withdrawFees(receiveEth, merkleBase); - - // It should set the ETH balance to 0. - assertEq(address(merkleBase).balance, 0, "merkle lockup eth balance"); - // It should transfer fee collected in ETH to the provided address. - assertEq(receiveEth.balance, defaults.DEFAULT_FEE(), "eth balance"); - } -} diff --git a/tests/integration/concrete/factory/withdraw-fees/withdrawFees.tree b/tests/integration/concrete/factory/withdraw-fees/withdrawFees.tree deleted file mode 100644 index eb437a1..0000000 --- a/tests/integration/concrete/factory/withdraw-fees/withdrawFees.tree +++ /dev/null @@ -1,21 +0,0 @@ -WithdrawFees_Integration_Test -├── when caller not admin -│ └── it should revert -└── when caller admin - ├── when withdrawal address zero - │ └── it should revert - └── when withdrawal address not zero - ├── when provided merkle lockup not valid - │ └── it should revert - └── when provided merkle lockup valid - ├── when provided address not contract - │ ├── it should transfer fee collected in ETH to the provided address - │ ├── it should set the ETH balance to 0 - │ └── it should emit {WithdrawFees} event - └── when provided address contract - ├── when provided address not implement receive eth - │ └── it should revert - └── when provided address implement receive eth - ├── it should transfer fee collected in ETH to the provided address - ├── it should set the ETH balance to 0 - └── it should emit {WithdrawFees} event diff --git a/tests/integration/concrete/instant/MerkleInstant.t.sol b/tests/integration/concrete/instant/MerkleInstant.t.sol index a8b8808..3000db7 100644 --- a/tests/integration/concrete/instant/MerkleInstant.t.sol +++ b/tests/integration/concrete/instant/MerkleInstant.t.sol @@ -4,10 +4,10 @@ pragma solidity >=0.8.22 <0.9.0; import { ISablierMerkleBase } from "src/interfaces/ISablierMerkleBase.sol"; import { Integration_Test } from "../../Integration.t.sol"; import { Clawback_Integration_Test } from "./../shared/clawback/clawback.t.sol"; +import { CollectFees_Integration_Test } from "./../shared/collect-fees/collectFees.t.sol"; 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 { WithdrawFees_Integration_Test } from "./../shared/withdraw-fees/withdrawFees.t.sol"; /*////////////////////////////////////////////////////////////////////////// NON-SHARED TESTS @@ -32,36 +32,36 @@ contract Clawback_MerkleInstant_Integration_Test is MerkleInstant_Integration_Sh } } -contract GetFirstClaimTime_MerkleInstant_Integration_Test is +contract CollectFees_MerkleInstant_Integration_Test is MerkleInstant_Integration_Shared_Test, - GetFirstClaimTime_Integration_Test + CollectFees_Integration_Test { function setUp() public override(MerkleInstant_Integration_Shared_Test, Integration_Test) { MerkleInstant_Integration_Shared_Test.setUp(); } } -contract HasClaimed_MerkleInstant_Integration_Test is +contract GetFirstClaimTime_MerkleInstant_Integration_Test is MerkleInstant_Integration_Shared_Test, - HasClaimed_Integration_Test + GetFirstClaimTime_Integration_Test { function setUp() public override(MerkleInstant_Integration_Shared_Test, Integration_Test) { MerkleInstant_Integration_Shared_Test.setUp(); } } -contract HasExpired_MerkleInstant_Integration_Test is +contract HasClaimed_MerkleInstant_Integration_Test is MerkleInstant_Integration_Shared_Test, - HasExpired_Integration_Test + HasClaimed_Integration_Test { function setUp() public override(MerkleInstant_Integration_Shared_Test, Integration_Test) { MerkleInstant_Integration_Shared_Test.setUp(); } } -contract WithdrawFees_MerkleInstant_Integration_Test is +contract HasExpired_MerkleInstant_Integration_Test is MerkleInstant_Integration_Shared_Test, - WithdrawFees_Integration_Test + HasExpired_Integration_Test { function setUp() public override(MerkleInstant_Integration_Shared_Test, Integration_Test) { MerkleInstant_Integration_Shared_Test.setUp(); diff --git a/tests/integration/concrete/instant/claim/claim.t.sol b/tests/integration/concrete/instant/claim/claim.t.sol index c2b7d3b..2f9460d 100644 --- a/tests/integration/concrete/instant/claim/claim.t.sol +++ b/tests/integration/concrete/instant/claim/claim.t.sol @@ -27,11 +27,11 @@ contract Claim_MerkleInstant_Integration_Test is Claim_Integration_Test, MerkleI emit ISablierMerkleInstant.Claim(defaults.INDEX1(), users.recipient1, defaults.CLAIM_AMOUNT()); expectCallToTransfer({ to: users.recipient1, value: defaults.CLAIM_AMOUNT() }); - expectCallToClaimWithMsgValue(address(merkleInstant), defaults.DEFAULT_FEE()); + expectCallToClaimWithMsgValue(address(merkleInstant), defaults.FEE()); claim(); assertTrue(merkleInstant.hasClaimed(defaults.INDEX1()), "not claimed"); - assertEq(address(merkleInstant).balance, previousFeeAccrued + defaults.DEFAULT_FEE(), "fee collected"); + assertEq(address(merkleInstant).balance, previousFeeAccrued + defaults.FEE(), "fee collected"); } } diff --git a/tests/integration/concrete/instant/constructor.t.sol b/tests/integration/concrete/instant/constructor.t.sol index bad55ee..c941e79 100644 --- a/tests/integration/concrete/instant/constructor.t.sol +++ b/tests/integration/concrete/instant/constructor.t.sol @@ -30,8 +30,7 @@ contract Constructor_MerkleInstant_Integration_Test is Integration_Test { // Make Factory the caller for the constructor test. resetPrank(address(merkleFactory)); - SablierMerkleInstant constructedInstant = - new SablierMerkleInstant(defaults.baseParams(), defaults.DEFAULT_FEE()); + SablierMerkleInstant constructedInstant = new SablierMerkleInstant(defaults.baseParams(), defaults.FEE()); Vars memory vars; @@ -64,7 +63,7 @@ contract Constructor_MerkleInstant_Integration_Test is Integration_Test { assertEq(bytes32(abi.encodePacked(vars.actualName)), vars.expectedName, "name"); vars.actualFee = constructedInstant.FEE(); - vars.expectedFee = defaults.DEFAULT_FEE(); + vars.expectedFee = defaults.FEE(); assertEq(vars.actualFee, vars.expectedFee, "fee"); } } diff --git a/tests/integration/concrete/ll/MerkleLL.t.sol b/tests/integration/concrete/ll/MerkleLL.t.sol index f7c4137..0ded970 100644 --- a/tests/integration/concrete/ll/MerkleLL.t.sol +++ b/tests/integration/concrete/ll/MerkleLL.t.sol @@ -4,10 +4,10 @@ pragma solidity >=0.8.22 <0.9.0; import { ISablierMerkleBase } from "src/interfaces/ISablierMerkleBase.sol"; import { Integration_Test } from "../../Integration.t.sol"; import { Clawback_Integration_Test } from "./../shared/clawback/clawback.t.sol"; +import { CollectFees_Integration_Test } from "./../shared/collect-fees/collectFees.t.sol"; 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 { WithdrawFees_Integration_Test } from "./../shared/withdraw-fees/withdrawFees.t.sol"; /*////////////////////////////////////////////////////////////////////////// NON-SHARED TESTS @@ -32,6 +32,12 @@ contract Clawback_MerkleLL_Integration_Test is MerkleLL_Integration_Shared_Test, } } +contract CollectFees_MerkleLL_Integration_Test is MerkleLL_Integration_Shared_Test, CollectFees_Integration_Test { + function setUp() public override(MerkleLL_Integration_Shared_Test, Integration_Test) { + MerkleLL_Integration_Shared_Test.setUp(); + } +} + contract GetFirstClaimTime_MerkleLL_Integration_Test is MerkleLL_Integration_Shared_Test, GetFirstClaimTime_Integration_Test @@ -52,9 +58,3 @@ contract HasExpired_MerkleLL_Integration_Test is MerkleLL_Integration_Shared_Tes MerkleLL_Integration_Shared_Test.setUp(); } } - -contract WithdrawFees_MerkleLL_Integration_Test is MerkleLL_Integration_Shared_Test, WithdrawFees_Integration_Test { - function setUp() public override(MerkleLL_Integration_Shared_Test, Integration_Test) { - MerkleLL_Integration_Shared_Test.setUp(); - } -} diff --git a/tests/integration/concrete/ll/claim/claim.t.sol b/tests/integration/concrete/ll/claim/claim.t.sol index 4f3efe9..df2cf30 100644 --- a/tests/integration/concrete/ll/claim/claim.t.sol +++ b/tests/integration/concrete/ll/claim/claim.t.sol @@ -62,7 +62,7 @@ contract Claim_MerkleLL_Integration_Test is Claim_Integration_Test, MerkleLL_Int /// @dev Helper function to test claim. function _test_Claim(uint40 startTime, uint40 cliffTime) private { - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); deal({ token: address(dai), to: address(merkleLL), give: defaults.AGGREGATE_AMOUNT() }); uint256 expectedStreamId = lockup.nextStreamId(); @@ -101,6 +101,6 @@ contract Claim_MerkleLL_Integration_Test is Claim_Integration_Test, MerkleLL_Int assertTrue(merkleLL.hasClaimed(defaults.INDEX1()), "not claimed"); - assertEq(address(merkleLL).balance, previousFeeAccrued + defaults.DEFAULT_FEE(), "fee collected"); + assertEq(address(merkleLL).balance, previousFeeAccrued + defaults.FEE(), "fee collected"); } } diff --git a/tests/integration/concrete/ll/constructor.t.sol b/tests/integration/concrete/ll/constructor.t.sol index f72dae6..9ca66a4 100644 --- a/tests/integration/concrete/ll/constructor.t.sol +++ b/tests/integration/concrete/ll/constructor.t.sol @@ -47,7 +47,7 @@ contract Constructor_MerkleLL_Integration_Test is Integration_Test { defaults.CANCELABLE(), defaults.TRANSFERABLE(), defaults.schedule(), - defaults.DEFAULT_FEE() + defaults.FEE() ); Vars memory vars; @@ -111,7 +111,7 @@ contract Constructor_MerkleLL_Integration_Test is Integration_Test { assertEq(vars.actualTransferable, vars.expectedTransferable, "transferable"); vars.actualFee = constructedLL.FEE(); - vars.expectedFee = defaults.DEFAULT_FEE(); + vars.expectedFee = defaults.FEE(); assertEq(vars.actualFee, vars.expectedFee, "fee"); } } diff --git a/tests/integration/concrete/lt/MerkleLT.t.sol b/tests/integration/concrete/lt/MerkleLT.t.sol index 03b4927..f802ebe 100644 --- a/tests/integration/concrete/lt/MerkleLT.t.sol +++ b/tests/integration/concrete/lt/MerkleLT.t.sol @@ -4,10 +4,10 @@ pragma solidity >=0.8.22 <0.9.0; import { ISablierMerkleBase } from "src/interfaces/ISablierMerkleBase.sol"; import { Integration_Test } from "../../Integration.t.sol"; import { Clawback_Integration_Test } from "./../shared/clawback/clawback.t.sol"; +import { CollectFees_Integration_Test } from "./../shared/collect-fees/collectFees.t.sol"; 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 { WithdrawFees_Integration_Test } from "./../shared/withdraw-fees/withdrawFees.t.sol"; /*////////////////////////////////////////////////////////////////////////// NON-SHARED TESTS @@ -32,6 +32,12 @@ contract Clawback_MerkleLT_Integration_Test is MerkleLT_Integration_Shared_Test, } } +contract CollectFees_MerkleLT_Integration_Test is MerkleLT_Integration_Shared_Test, CollectFees_Integration_Test { + function setUp() public override(MerkleLT_Integration_Shared_Test, Integration_Test) { + MerkleLT_Integration_Shared_Test.setUp(); + } +} + contract GetFirstClaimTime_MerkleLT_Integration_Test is MerkleLT_Integration_Shared_Test, GetFirstClaimTime_Integration_Test @@ -52,9 +58,3 @@ contract HasExpired_MerkleLT_Integration_Test is MerkleLT_Integration_Shared_Tes MerkleLT_Integration_Shared_Test.setUp(); } } - -contract WithdrawFees_MerkleLT_Integration_Test is MerkleLT_Integration_Shared_Test, WithdrawFees_Integration_Test { - function setUp() public override(MerkleLT_Integration_Shared_Test, Integration_Test) { - MerkleLT_Integration_Shared_Test.setUp(); - } -} diff --git a/tests/integration/concrete/lt/claim/claim.t.sol b/tests/integration/concrete/lt/claim/claim.t.sol index 551ae1a..c595ec3 100644 --- a/tests/integration/concrete/lt/claim/claim.t.sol +++ b/tests/integration/concrete/lt/claim/claim.t.sol @@ -16,7 +16,7 @@ contract Claim_MerkleLT_Integration_Test is Claim_Integration_Test, MerkleLT_Int } function test_RevertWhen_TotalPercentageLessThan100() external whenMerkleProofValid whenTotalPercentageNot100 { - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); // Create a MerkleLT campaign with a total percentage less than 100. MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); @@ -53,7 +53,7 @@ contract Claim_MerkleLT_Integration_Test is Claim_Integration_Test, MerkleLT_Int } function test_RevertWhen_TotalPercentageGreaterThan100() external whenMerkleProofValid whenTotalPercentageNot100 { - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); // Create a MerkleLT campaign with a total percentage less than 100. MerkleLT.TrancheWithPercentage[] memory tranchesWithPercentages = defaults.tranchesWithPercentages(); tranchesWithPercentages[0].unlockPercentage = ud2x18(0.75e18); @@ -114,7 +114,7 @@ contract Claim_MerkleLT_Integration_Test is Claim_Integration_Test, MerkleLT_Int /// @dev Helper function to test claim. function _test_Claim(uint40 streamStartTime, uint40 startTime) private { - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); deal({ token: address(dai), to: address(merkleLT), give: defaults.AGGREGATE_AMOUNT() }); @@ -154,6 +154,6 @@ contract Claim_MerkleLT_Integration_Test is Claim_Integration_Test, MerkleLT_Int assertTrue(merkleLT.hasClaimed(defaults.INDEX1()), "not claimed"); - assertEq(address(merkleLT).balance, previousFeeAccrued + defaults.DEFAULT_FEE(), "fee collected"); + assertEq(address(merkleLT).balance, previousFeeAccrued + defaults.FEE(), "fee collected"); } } diff --git a/tests/integration/concrete/lt/constructor.t.sol b/tests/integration/concrete/lt/constructor.t.sol index 356e9db..d4fc645 100644 --- a/tests/integration/concrete/lt/constructor.t.sol +++ b/tests/integration/concrete/lt/constructor.t.sol @@ -52,7 +52,7 @@ contract Constructor_MerkleLT_Integration_Test is Integration_Test { defaults.TRANSFERABLE(), defaults.STREAM_START_TIME_ZERO(), defaults.tranchesWithPercentages(), - defaults.DEFAULT_FEE() + defaults.FEE() ); Vars memory vars; @@ -98,7 +98,7 @@ contract Constructor_MerkleLT_Integration_Test is Integration_Test { assertEq(bytes32(abi.encodePacked(vars.actualName)), vars.expectedName, "name"); vars.actualFee = constructedLT.FEE(); - vars.expectedFee = defaults.DEFAULT_FEE(); + vars.expectedFee = defaults.FEE(); assertEq(vars.actualFee, vars.expectedFee, "fee"); vars.actualStreamStartTime = constructedLT.STREAM_START_TIME(); diff --git a/tests/integration/concrete/shared/claim/claim.t.sol b/tests/integration/concrete/shared/claim/claim.t.sol index 26829e2..441aefe 100644 --- a/tests/integration/concrete/shared/claim/claim.t.sol +++ b/tests/integration/concrete/shared/claim/claim.t.sol @@ -8,7 +8,7 @@ import { Integration_Test } from "../../../Integration.t.sol"; abstract contract Claim_Integration_Test is Integration_Test { function test_RevertGiven_CampaignExpired() external { uint40 expiration = defaults.EXPIRATION(); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); uint256 warpTime = expiration + 1 seconds; bytes32[] memory merkleProof; vm.warp({ newTimestamp: warpTime }); @@ -20,7 +20,7 @@ abstract contract Claim_Integration_Test is Integration_Test { uint256 index1 = defaults.INDEX1(); uint128 amount = defaults.CLAIM_AMOUNT(); bytes32[] memory merkleProof = defaults.index1Proof(); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleBase_InsufficientFeePayment.selector, 0, fee)); merkleBase.claim{ value: 0 }(index1, users.recipient1, amount, merkleProof); @@ -30,7 +30,7 @@ abstract contract Claim_Integration_Test is Integration_Test { claim(); uint256 index1 = defaults.INDEX1(); uint128 amount = defaults.CLAIM_AMOUNT(); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); bytes32[] memory merkleProof = defaults.index1Proof(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleBase_StreamClaimed.selector, index1)); merkleBase.claim{ value: fee }(index1, users.recipient1, amount, merkleProof); @@ -44,7 +44,7 @@ abstract contract Claim_Integration_Test is Integration_Test { { uint256 invalidIndex = 1337; uint128 amount = defaults.CLAIM_AMOUNT(); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); bytes32[] memory merkleProof = defaults.index1Proof(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleBase_InvalidProof.selector)); merkleBase.claim{ value: fee }(invalidIndex, users.recipient1, amount, merkleProof); @@ -60,7 +60,7 @@ abstract contract Claim_Integration_Test is Integration_Test { uint256 index1 = defaults.INDEX1(); address invalidRecipient = address(1337); uint128 amount = defaults.CLAIM_AMOUNT(); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); bytes32[] memory merkleProof = defaults.index1Proof(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleBase_InvalidProof.selector)); merkleBase.claim{ value: fee }(index1, invalidRecipient, amount, merkleProof); @@ -76,7 +76,7 @@ abstract contract Claim_Integration_Test is Integration_Test { { uint256 index1 = defaults.INDEX1(); uint128 invalidAmount = 1337; - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); bytes32[] memory merkleProof = defaults.index1Proof(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleBase_InvalidProof.selector)); merkleBase.claim{ value: fee }(index1, users.recipient1, invalidAmount, merkleProof); @@ -93,7 +93,7 @@ abstract contract Claim_Integration_Test is Integration_Test { { uint256 index1 = defaults.INDEX1(); uint128 amount = defaults.CLAIM_AMOUNT(); - uint256 fee = defaults.DEFAULT_FEE(); + uint256 fee = defaults.FEE(); bytes32[] memory invalidMerkleProof = defaults.index2Proof(); vm.expectRevert(abi.encodeWithSelector(Errors.SablierMerkleBase_InvalidProof.selector)); merkleBase.claim{ value: fee }(index1, users.recipient1, amount, invalidMerkleProof); diff --git a/tests/integration/concrete/shared/collect-fees/collectFees.t.sol b/tests/integration/concrete/shared/collect-fees/collectFees.t.sol new file mode 100644 index 0000000..d280413 --- /dev/null +++ b/tests/integration/concrete/shared/collect-fees/collectFees.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { Errors } from "src/libraries/Errors.sol"; + +import { Integration_Test } from "../../../Integration.t.sol"; + +abstract contract CollectFees_Integration_Test is Integration_Test { + function test_RevertWhen_CallerNotFactory() external { + // Set the caller to anything other than the factory. + resetPrank(users.admin); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierMerkleBase_CallerNotFactory.selector, address(merkleFactory), users.admin + ) + ); + merkleBase.collectFees(users.admin); + } + + modifier whenCallerFactory() { + // Claim to collect some fees. + claim(); + + resetPrank(address(merkleFactory)); + _; + } + + function test_WhenFactoryAdminIsNotContract() external whenCallerFactory { + _test_CollectFees(users.admin); + } + + function test_RevertWhen_FactoryAdminDoesNotImplementReceiveFunction() + external + whenCallerFactory + whenFactoryAdminIsContract + { + // Transfer the admin to a contract that implements the receive function. + resetPrank({ msgSender: users.admin }); + merkleFactory.transferAdmin(address(contractWithoutReceiveEth)); + + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierMerkleBase_FeeTransferFail.selector, + address(contractWithoutReceiveEth), + address(merkleBase).balance + ) + ); + + resetPrank(address(merkleFactory)); + merkleBase.collectFees(address(contractWithoutReceiveEth)); + } + + function test_WhenFactoryAdminImplementsReceiveFunction() external whenCallerFactory whenFactoryAdminIsContract { + // Transfer the admin to a contract that implements the receive function. + resetPrank({ msgSender: users.admin }); + merkleFactory.transferAdmin(address(contractWithoutReceiveEth)); + + _test_CollectFees(address(contractWithReceiveEth)); + } + + function _test_CollectFees(address admin) private { + // Load the initial ETH balance of the admin. + uint256 initialAdminBalance = admin.balance; + + resetPrank(address(merkleFactory)); + merkleBase.collectFees(admin); + + // It should set the ETH balance to 0. + assertEq(address(merkleBase).balance, 0, "merkle lockup ETH balance"); + // It should transfer fee collected in ETH to the factory admin. + assertEq(admin.balance, initialAdminBalance + defaults.FEE(), "admin ETH balance"); + } +} diff --git a/tests/integration/concrete/shared/withdraw-fees/withdrawFees.tree b/tests/integration/concrete/shared/collect-fees/collectFees.tree similarity index 56% rename from tests/integration/concrete/shared/withdraw-fees/withdrawFees.tree rename to tests/integration/concrete/shared/collect-fees/collectFees.tree index b5839e6..f911089 100644 --- a/tests/integration/concrete/shared/withdraw-fees/withdrawFees.tree +++ b/tests/integration/concrete/shared/collect-fees/collectFees.tree @@ -1,13 +1,13 @@ -WithdrawFees_Integration_Test +CollectFees_Integration_Test ├── when caller not factory │ └── it should revert └── when caller factory - ├── when provided address not contract - │ ├── it should transfer fee collected in ETH to the provided address + ├── when factory admin is not contract + │ ├── it should transfer fee collected in ETH to the factory admin │ └── it should set the ETH balance to 0 - └── when provided address contract - ├── when provided address not implement receive eth + └── when factory admin is contract + ├── when factory admin does not implement receive function │ └── it should revert - └── when provided address implement receive eth - ├── it should transfer fee collected in ETH to the provided address + └── when factory admin implements receive function + ├── it should transfer fee collected in ETH to the factory admin └── it should set the ETH balance to 0 diff --git a/tests/integration/concrete/shared/withdraw-fees/withdrawFees.t.sol b/tests/integration/concrete/shared/withdraw-fees/withdrawFees.t.sol deleted file mode 100644 index 4d283a8..0000000 --- a/tests/integration/concrete/shared/withdraw-fees/withdrawFees.t.sol +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.22 <0.9.0; - -import { Errors } from "src/libraries/Errors.sol"; - -import { Integration_Test } from "../../../Integration.t.sol"; - -abstract contract WithdrawFees_Integration_Test is Integration_Test { - function test_RevertWhen_CallerNotFactory() external { - // Set the caller to anything other than the factory. - resetPrank(users.admin); - - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierMerkleBase_CallerNotFactory.selector, address(merkleFactory), users.admin - ) - ); - merkleBase.withdrawFees(users.admin); - } - - modifier whenCallerFactory() { - // Claim to collect some fees. - claim(); - - resetPrank(address(merkleFactory)); - _; - } - - function test_WhenProvidedAddressNotContract() external whenCallerFactory { - uint256 previousToBalance = users.admin.balance; - - merkleBase.withdrawFees(users.admin); - - // It should set the ETH balance to 0. - assertEq(address(merkleBase).balance, 0, "merkle lockup eth balance"); - // It should transfer fee collected in ETH to the provided address. - assertEq(users.admin.balance, previousToBalance + defaults.DEFAULT_FEE(), "eth balance"); - } - - function test_RevertWhen_ProvidedAddressNotImplementReceiveEth() - external - whenCallerFactory - whenProvidedAddressContract - { - address payable noReceiveEth = payable(address(contractWithoutReceiveEth)); - vm.expectRevert( - abi.encodeWithSelector( - Errors.SablierMerkleBase_FeeWithdrawFailed.selector, noReceiveEth, address(merkleBase).balance - ) - ); - merkleBase.withdrawFees(noReceiveEth); - } - - function test_WhenProvidedAddressImplementReceiveEth() external whenCallerFactory whenProvidedAddressContract { - address payable receiveEth = payable(address(contractWithReceiveEth)); - - merkleBase.withdrawFees(receiveEth); - - // It should set the ETH balance to 0. - assertEq(address(merkleBase).balance, 0, "merkle lockup eth balance"); - // It should transfer fee collected in ETH to the provided address. - assertEq(receiveEth.balance, defaults.DEFAULT_FEE(), "eth balance"); - } -} diff --git a/tests/utils/Defaults.sol b/tests/utils/Defaults.sol index 0cb6e84..a4daad5 100644 --- a/tests/utils/Defaults.sol +++ b/tests/utils/Defaults.sol @@ -41,8 +41,8 @@ contract Defaults is Constants, Merkle { uint256 public constant AGGREGATE_AMOUNT = CLAIM_AMOUNT * RECIPIENT_COUNT; bool public constant CANCELABLE = false; uint128 public constant CLAIM_AMOUNT = 10_000e18; - uint256 public constant DEFAULT_FEE = 0.005e18; uint40 public immutable EXPIRATION; + uint256 public constant FEE = 0.005e18; uint40 public constant FIRST_CLAIM_TIME = JULY_1_2024; uint256 public constant INDEX1 = 1; uint256 public constant INDEX2 = 2; diff --git a/tests/utils/Modifiers.sol b/tests/utils/Modifiers.sol index 0b2a6e4..bf1fd85 100644 --- a/tests/utils/Modifiers.sol +++ b/tests/utils/Modifiers.sol @@ -67,6 +67,10 @@ abstract contract Modifiers is Utils { _; } + modifier whenFactoryAdminIsContract() { + _; + } + modifier whenIndexInMerkleTree() { _; } @@ -91,10 +95,6 @@ abstract contract Modifiers is Utils { _; } - modifier whenProvidedAddressContract() { - _; - } - modifier whenProvidedMerkleLockupValid() { _; }