From 1df87d720984b067818ce3896dc0772855d9e2da Mon Sep 17 00:00:00 2001 From: Andrei Vlad Birgaoanu <99738872+andreivladbrg@users.noreply.github.com> Date: Mon, 16 Dec 2024 06:44:02 +0200 Subject: [PATCH] feat: add eth fees in flow functions (#348) * feat: add eth fees in flow functions test: update tests accordingly * test: add concrete tests for payable functions * address feedback * chore: remove redundant function Signed-off-by: smol-ninja --------- Signed-off-by: smol-ninja Co-authored-by: smol-ninja --- src/SablierFlow.sol | 15 ++++ src/abstracts/Batch.sol | 2 +- src/abstracts/SablierFlowBase.sol | 16 ++++ src/interfaces/IBatch.sol | 2 +- src/interfaces/ISablierFlow.sol | 27 +++--- src/interfaces/ISablierFlowBase.sol | 13 +++ src/libraries/Errors.sol | 3 + tests/Base.t.sol | 14 +++- tests/fork/Flow.t.sol | 32 ++++--- tests/fork/Fork.t.sol | 6 +- .../concrete/collect-fees/collectFees.t.sol | 71 ++++++++++++++++ .../concrete/collect-fees/collectFees.tree | 12 +++ .../concrete/payable/payable.t.sol | 84 +++++++++++++++++++ .../integration/concrete/payable/payable.tree | 59 +++++++++++++ tests/mocks/Receive.sol | 8 ++ tests/utils/Constants.sol | 1 + tests/utils/Modifiers.sol | 8 ++ 17 files changed, 345 insertions(+), 28 deletions(-) create mode 100644 tests/integration/concrete/collect-fees/collectFees.t.sol create mode 100644 tests/integration/concrete/collect-fees/collectFees.tree create mode 100644 tests/integration/concrete/payable/payable.t.sol create mode 100644 tests/integration/concrete/payable/payable.tree create mode 100644 tests/mocks/Receive.sol diff --git a/src/SablierFlow.sol b/src/SablierFlow.sol index ea5c7cd1..350101a7 100644 --- a/src/SablierFlow.sol +++ b/src/SablierFlow.sol @@ -191,6 +191,7 @@ contract SablierFlow is UD21x18 newRatePerSecond ) external + payable override noDelegateCall notNull(streamId) @@ -221,6 +222,7 @@ contract SablierFlow is bool transferable ) external + payable override noDelegateCall returns (uint256 streamId) @@ -239,6 +241,7 @@ contract SablierFlow is uint128 amount ) external + payable override noDelegateCall returns (uint256 streamId) @@ -258,6 +261,7 @@ contract SablierFlow is address recipient ) external + payable override noDelegateCall notNull(streamId) @@ -277,6 +281,7 @@ contract SablierFlow is uint128 amount ) external + payable override noDelegateCall notNull(streamId) @@ -300,6 +305,7 @@ contract SablierFlow is Broker calldata broker ) external + payable override noDelegateCall notNull(streamId) @@ -316,6 +322,7 @@ contract SablierFlow is /// @inheritdoc ISablierFlow function pause(uint256 streamId) external + payable override noDelegateCall notNull(streamId) @@ -333,6 +340,7 @@ contract SablierFlow is uint128 amount ) external + payable override noDelegateCall notNull(streamId) @@ -349,6 +357,7 @@ contract SablierFlow is uint128 amount ) external + payable override noDelegateCall notNull(streamId) @@ -366,6 +375,7 @@ contract SablierFlow is /// @inheritdoc ISablierFlow function refundMax(uint256 streamId) external + payable override noDelegateCall notNull(streamId) @@ -384,6 +394,7 @@ contract SablierFlow is UD21x18 ratePerSecond ) external + payable override noDelegateCall notNull(streamId) @@ -402,6 +413,7 @@ contract SablierFlow is uint128 amount ) external + payable override noDelegateCall notNull(streamId) @@ -419,6 +431,7 @@ contract SablierFlow is /// @inheritdoc ISablierFlow function void(uint256 streamId) external + payable override noDelegateCall notNull(streamId) @@ -436,6 +449,7 @@ contract SablierFlow is uint128 amount ) external + payable override noDelegateCall notNull(streamId) @@ -452,6 +466,7 @@ contract SablierFlow is address to ) external + payable override noDelegateCall notNull(streamId) diff --git a/src/abstracts/Batch.sol b/src/abstracts/Batch.sol index 47a86d48..d6d2f7e0 100644 --- a/src/abstracts/Batch.sol +++ b/src/abstracts/Batch.sol @@ -13,7 +13,7 @@ abstract contract Batch is IBatch { //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IBatch - function batch(bytes[] calldata calls) external override { + function batch(bytes[] calldata calls) external payable override { uint256 count = calls.length; for (uint256 i = 0; i < count; ++i) { diff --git a/src/abstracts/SablierFlowBase.sol b/src/abstracts/SablierFlowBase.sol index 3770fba7..9cde2755 100644 --- a/src/abstracts/SablierFlowBase.sol +++ b/src/abstracts/SablierFlowBase.sol @@ -205,6 +205,22 @@ abstract contract SablierFlowBase is USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc ISablierFlowBase + function collectFees() external override { + uint256 feeAmount = address(this).balance; + + // Effect: transfer the fees to the admin. + (bool success,) = admin.call{ value: feeAmount }(""); + + // Revert if the call failed. + if (!success) { + revert Errors.SablierFlowBase_FeeTransferFail(admin, feeAmount); + } + + // Log the fee withdrawal. + emit ISablierFlowBase.CollectFees({ admin: admin, feeAmount: feeAmount }); + } + /// @inheritdoc ISablierFlowBase function collectProtocolRevenue(IERC20 token, address to) external override onlyAdmin { uint128 revenue = protocolRevenue[token]; diff --git a/src/interfaces/IBatch.sol b/src/interfaces/IBatch.sol index b4125adb..862762ef 100644 --- a/src/interfaces/IBatch.sol +++ b/src/interfaces/IBatch.sol @@ -5,5 +5,5 @@ pragma solidity >=0.8.22; interface IBatch { /// @notice Allows batched call to self, `this` contract. /// @param calls An array of inputs for each call. - function batch(bytes[] calldata calls) external; + function batch(bytes[] calldata calls) external payable; } diff --git a/src/interfaces/ISablierFlow.sol b/src/interfaces/ISablierFlow.sol index bb2c5a08..593cb8cb 100644 --- a/src/interfaces/ISablierFlow.sol +++ b/src/interfaces/ISablierFlow.sol @@ -178,7 +178,7 @@ interface ISablierFlow is /// @param streamId The ID of the stream to adjust. /// @param newRatePerSecond The new rate per second, denoted as a fixed-point number where 1e18 is 1 token /// per second. - function adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) external; + function adjustRatePerSecond(uint256 streamId, UD21x18 newRatePerSecond) external payable; /// @notice Creates a new Flow stream by setting the snapshot time to `block.timestamp` and leaving the balance to /// zero. The stream is wrapped in an ERC-721 NFT. @@ -208,6 +208,7 @@ interface ISablierFlow is bool transferable ) external + payable returns (uint256 streamId); /// @notice Creates a new Flow stream by setting the snapshot time to `block.timestamp` and the balance to `amount`. @@ -239,6 +240,7 @@ interface ISablierFlow is uint128 amount ) external + payable returns (uint256 streamId); /// @notice Makes a deposit in a stream. @@ -255,7 +257,7 @@ interface ISablierFlow is /// @param amount The deposit amount, denoted in token's decimals. /// @param sender The stream's sender address. /// @param recipient The stream's recipient address. - function deposit(uint256 streamId, uint128 amount, address sender, address recipient) external; + function deposit(uint256 streamId, uint128 amount, address sender, address recipient) external payable; /// @notice Deposits tokens in a stream and pauses it. /// @@ -269,7 +271,7 @@ interface ISablierFlow is /// /// @param streamId The ID of the stream to deposit to, and then pause. /// @param amount The deposit amount, denoted in token's decimals. - function depositAndPause(uint256 streamId, uint128 amount) external; + function depositAndPause(uint256 streamId, uint128 amount) external payable; /// @notice Deposits tokens in a stream. /// @@ -298,7 +300,8 @@ interface ISablierFlow is address recipient, Broker calldata broker ) - external; + external + payable; /// @notice Pauses the stream. /// @@ -314,7 +317,7 @@ interface ISablierFlow is /// - `msg.sender` must be the stream's sender. /// /// @param streamId The ID of the stream to pause. - function pause(uint256 streamId) external; + function pause(uint256 streamId) external payable; /// @notice Refunds the provided amount of tokens from the stream to the sender's address. /// @@ -328,7 +331,7 @@ interface ISablierFlow is /// /// @param streamId The ID of the stream to refund from. /// @param amount The amount to refund, denoted in token's decimals. - function refund(uint256 streamId, uint128 amount) external; + function refund(uint256 streamId, uint128 amount) external payable; /// @notice Refunds the provided amount of tokens from the stream to the sender's address. /// @@ -342,7 +345,7 @@ interface ISablierFlow is /// /// @param streamId The ID of the stream to refund from and then pause. /// @param amount The amount to refund, denoted in token's decimals. - function refundAndPause(uint256 streamId, uint128 amount) external; + function refundAndPause(uint256 streamId, uint128 amount) external payable; /// @notice Refunds the entire refundable amount of tokens from the stream to the sender's address. /// @@ -352,7 +355,7 @@ interface ISablierFlow is /// - Refer to the requirements in {refund}. /// /// @param streamId The ID of the stream to refund from. - function refundMax(uint256 streamId) external; + function refundMax(uint256 streamId) external payable; /// @notice Restarts the stream with the provided rate per second. /// @@ -370,7 +373,7 @@ interface ISablierFlow is /// @param streamId The ID of the stream to restart. /// @param ratePerSecond The amount by which the debt is increasing every second, denoted as a fixed-point number /// where 1e18 is 1 token per second. - function restart(uint256 streamId, UD21x18 ratePerSecond) external; + function restart(uint256 streamId, UD21x18 ratePerSecond) external payable; /// @notice Restarts the stream with the provided rate per second, and makes a deposit. /// @@ -387,7 +390,7 @@ interface ISablierFlow is /// @param ratePerSecond The amount by which the debt is increasing every second, denoted as a fixed-point number /// where 1e18 is 1 token per second. /// @param amount The deposit amount, denoted in token's decimals. - function restartAndDeposit(uint256 streamId, UD21x18 ratePerSecond, uint128 amount) external; + function restartAndDeposit(uint256 streamId, UD21x18 ratePerSecond, uint128 amount) external payable; /// @notice Voids a stream. /// @@ -407,7 +410,7 @@ interface ISablierFlow is /// - `msg.sender` must either be the stream's sender, recipient or an approved third party. /// /// @param streamId The ID of the stream to void. - function void(uint256 streamId) external; + function void(uint256 streamId) external payable; /// @notice Withdraws the provided `amount` minus the protocol fee to the provided `to` address. /// @@ -436,6 +439,7 @@ interface ISablierFlow is uint128 amount ) external + payable returns (uint128 withdrawnAmount, uint128 protocolFeeAmount); /// @notice Withdraws the entire withdrawable amount minus the protocol fee to the provided `to` address. @@ -458,5 +462,6 @@ interface ISablierFlow is address to ) external + payable returns (uint128 withdrawnAmount, uint128 protocolFeeAmount); } diff --git a/src/interfaces/ISablierFlowBase.sol b/src/interfaces/ISablierFlowBase.sol index 164d7857..16de29cb 100644 --- a/src/interfaces/ISablierFlowBase.sol +++ b/src/interfaces/ISablierFlowBase.sol @@ -19,6 +19,11 @@ interface ISablierFlowBase is IERC721Metadata, // 2 inherited components IAdminable // 0 inherited components { + /// @notice Emitted when the accrued fees are collected. + /// @param admin The address of the current contract admin, which has received the fees. + /// @param feeAmount The amount of collected fees. + event CollectFees(address indexed admin, uint256 indexed feeAmount); + /// @notice Emitted when the contract admin collects protocol revenue accrued. /// @param admin The address of the contract admin. /// @param token The address of the ERC-20 token the protocol revenue has been collected for. @@ -145,6 +150,14 @@ interface ISablierFlowBase is NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @notice Collects the accrued fees by transferring them to the contract admin. + /// + /// @dev Emits a {CollectFees} event. + /// + /// Notes: + /// - If the admin is a contract, it must be able to receive ETH. + function collectFees() external; + /// @notice Collect the protocol revenue accrued for the provided ERC-20 token. /// /// @dev Emits {CollectProtocolRevenue} event. diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 080bcd3c..2b2821d8 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -91,6 +91,9 @@ library Errors { SABLIER-FLOW-BASE //////////////////////////////////////////////////////////////////////////*/ + /// @notice Thrown when the fee transfer fails. + error SablierFlowBase_FeeTransferFail(address admin, uint256 feeAmount); + /// @notice Thrown when trying to claim protocol revenue when the accrued amount is zero. error SablierFlowBase_NoProtocolRevenue(address token); diff --git a/tests/Base.t.sol b/tests/Base.t.sol index f89e951b..9628702c 100644 --- a/tests/Base.t.sol +++ b/tests/Base.t.sol @@ -8,6 +8,7 @@ import { ISablierFlow } from "src/interfaces/ISablierFlow.sol"; import { SablierFlow } from "src/SablierFlow.sol"; import { ERC20MissingReturn } from "./mocks/ERC20MissingReturn.sol"; import { ERC20Mock } from "./mocks/ERC20Mock.sol"; +import { ContractWithoutReceive, ContractWithReceive } from "./mocks/Receive.sol"; import { Assertions } from "./utils/Assertions.sol"; import { Modifiers } from "./utils/Modifiers.sol"; import { Users } from "./utils/Types.sol"; @@ -25,9 +26,11 @@ abstract contract Base_Test is Assertions, Modifiers, Test { TEST CONTRACTS //////////////////////////////////////////////////////////////////////////*/ + ContractWithoutReceive internal contractWithoutReceive; + ContractWithReceive internal contractWithReceive; + ERC20Mock internal dai; ERC20Mock internal tokenWithoutDecimals; ERC20Mock internal tokenWithProtocolFee; - ERC20Mock internal dai; ERC20Mock internal usdc; ERC20MissingReturn internal usdt; @@ -48,8 +51,13 @@ abstract contract Base_Test is Assertions, Modifiers, Test { flow = deployOptimizedSablierFlow(); } - // Label the flow contract. - vm.label(address(flow), "Flow"); + contractWithoutReceive = new ContractWithoutReceive(); + contractWithReceive = new ContractWithReceive(); + + // Label the contracts. + vm.label({ account: address(flow), newLabel: "Flow" }); + vm.label({ account: address(contractWithoutReceive), newLabel: "Contract without Receive" }); + vm.label({ account: address(contractWithReceive), newLabel: "Contract with Receive" }); // Create new tokens and label them. createAndLabelTokens(); diff --git a/tests/fork/Flow.t.sol b/tests/fork/Flow.t.sol index 7229a229..89f3a58d 100644 --- a/tests/fork/Flow.t.sol +++ b/tests/fork/Flow.t.sol @@ -95,6 +95,9 @@ contract Flow_Fork_Test is Fork_Test { // Make sure that fuzzed users don't overlap with Flow address. checkUsers(params.recipient, params.sender); + // Make sure that the sender has enough funds. + vm.deal({ account: params.sender, newBalance: 1_000_000 ether }); + // Warp to a different time. params.timeJump = _passTime(params.timeJump); @@ -148,6 +151,9 @@ contract Flow_Fork_Test is Fork_Test { ) private { + uint256 initialFlowBalance = address(flow).balance; + + // Each function is going to pay a fee. if (flowFunc == FlowFunc.adjustRatePerSecond) { _test_AdjustRatePerSecond(streamId, ratePerSecond); } else if (flowFunc == FlowFunc.deposit) { @@ -163,6 +169,10 @@ contract Flow_Fork_Test is Fork_Test { } else if (flowFunc == FlowFunc.withdraw) { _test_Withdraw(streamId, withdrawAmount); } + + // Assert that the flow balance has changed. + uint256 expectedFlowBalance = initialFlowBalance + FEE; + assertEq(address(flow).balance, expectedFlowBalance, "Flow balance"); } /// @notice Find the first non-voided stream ID with the same token. @@ -247,7 +257,7 @@ contract Flow_Fork_Test is Fork_Test { vm.expectEmit({ emitter: address(flow) }); emit IERC4906.MetadataUpdate({ _tokenId: streamId }); - flow.adjustRatePerSecond({ streamId: streamId, newRatePerSecond: newRatePerSecond }); + flow.adjustRatePerSecond{ value: FEE }({ streamId: streamId, newRatePerSecond: newRatePerSecond }); // It should update snapshot debt. vars.actualSnapshotDebtScaled = flow.getSnapshotDebtScaled(streamId); @@ -287,7 +297,7 @@ contract Flow_Fork_Test is Fork_Test { transferable: transferable }); - vars.actualStreamId = flow.create({ + vars.actualStreamId = flow.create{ value: FEE }({ recipient: recipient, sender: sender, ratePerSecond: ratePerSecond, @@ -357,7 +367,7 @@ contract Flow_Fork_Test is Fork_Test { expectCallToTransferFrom({ token: token, from: sender, to: address(flow), amount: depositAmount }); // Make the deposit. - flow.deposit(streamId, depositAmount, sender, flow.getRecipient(streamId)); + flow.deposit{ value: FEE }(streamId, depositAmount, sender, flow.getRecipient(streamId)); // Assert that the token balance of stream has been updated. vars.actualTokenBalance = token.balanceOf(address(flow)); @@ -381,7 +391,8 @@ contract Flow_Fork_Test is Fork_Test { function _test_Pause(uint256 streamId) private { // Make sure the requirements are respected. - resetPrank({ msgSender: flow.getSender(streamId) }); + address sender = flow.getSender(streamId); + resetPrank({ msgSender: sender }); if (flow.isPaused(streamId)) { flow.restart(streamId, RATE_PER_SECOND); } @@ -390,7 +401,7 @@ contract Flow_Fork_Test is Fork_Test { vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.PauseFlowStream({ streamId: streamId, - sender: flow.getSender(streamId), + sender: sender, recipient: flow.getRecipient(streamId), totalDebt: flow.totalDebtOf(streamId) }); @@ -399,7 +410,7 @@ contract Flow_Fork_Test is Fork_Test { emit IERC4906.MetadataUpdate({ _tokenId: streamId }); // Pause the stream. - flow.pause(streamId); + flow.pause{ value: FEE }(streamId); // Assert that the stream is paused. assertTrue(flow.isPaused(streamId), "Pause: paused"); @@ -442,7 +453,7 @@ contract Flow_Fork_Test is Fork_Test { emit IERC4906.MetadataUpdate({ _tokenId: streamId }); // Request the refund. - flow.refund(streamId, refundAmount); + flow.refund{ value: FEE }(streamId, refundAmount); // Assert that the token balance of stream has been updated. vars.actualTokenBalance = token.balanceOf(address(flow)); @@ -468,6 +479,7 @@ contract Flow_Fork_Test is Fork_Test { // Make sure the requirements are respected. address sender = flow.getSender(streamId); resetPrank({ msgSender: sender }); + if (!flow.isPaused(streamId)) { flow.pause(streamId); } @@ -482,7 +494,7 @@ contract Flow_Fork_Test is Fork_Test { vm.expectEmit({ emitter: address(flow) }); emit IERC4906.MetadataUpdate({ _tokenId: streamId }); - flow.restart({ streamId: streamId, ratePerSecond: ratePerSecond }); + flow.restart{ value: FEE }({ streamId: streamId, ratePerSecond: ratePerSecond }); // It should restart the stream. assertFalse(flow.isPaused(streamId)); @@ -529,7 +541,7 @@ contract Flow_Fork_Test is Fork_Test { vm.expectEmit({ emitter: address(flow) }); emit IERC4906.MetadataUpdate({ _tokenId: streamId }); - flow.void(streamId); + flow.void{ value: FEE }(streamId); // It should set the rate per second to zero. assertEq(flow.getRatePerSecond(streamId), 0, "Void: rate per second"); @@ -590,7 +602,7 @@ contract Flow_Fork_Test is Fork_Test { emit IERC4906.MetadataUpdate({ _tokenId: streamId }); // Withdraw the tokens. - flow.withdraw(streamId, recipient, withdrawAmount); + flow.withdraw{ value: FEE }(streamId, recipient, withdrawAmount); // It should update snapshot time. vars.actualSnapshotTime = flow.getSnapshotTime(streamId); diff --git a/tests/fork/Fork.t.sol b/tests/fork/Fork.t.sol index 41b8fac5..20b050ec 100644 --- a/tests/fork/Fork.t.sol +++ b/tests/fork/Fork.t.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.22; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ISablierFlow } from "src/interfaces/ISablierFlow.sol"; +import { SablierFlow } from "src/SablierFlow.sol"; import { Base_Test } from "../Base.t.sol"; @@ -41,8 +42,9 @@ abstract contract Fork_Test is Base_Test { // Fork Ethereum Mainnet at a block number after the Sablier deployment. vm.createSelectFork({ blockNumber: 21_330_578, urlOrAlias: "mainnet" }); - // Load mainnet address of flow. - flow = ISablierFlow(0x2D9221a63E12AA796619cb381Ec4A71b201281f5); + // TODO: update the flow address once deployed. + // flow = ISablierFlow(0x2D9221a63E12AA796619cb381Ec4A71b201281f5); + flow = new SablierFlow(users.admin, nftDescriptor); // Label the flow contract. vm.label(address(flow), "Flow"); diff --git a/tests/integration/concrete/collect-fees/collectFees.t.sol b/tests/integration/concrete/collect-fees/collectFees.t.sol new file mode 100644 index 00000000..4dd80d24 --- /dev/null +++ b/tests/integration/concrete/collect-fees/collectFees.t.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierFlowBase } from "src/interfaces/ISablierFlowBase.sol"; +import { Errors } from "src/libraries/Errors.sol"; + +import { Shared_Integration_Concrete_Test } from "../Concrete.t.sol"; + +contract CollectFees_Integration_Concrete_Test is Shared_Integration_Concrete_Test { + function setUp() public override { + Shared_Integration_Concrete_Test.setUp(); + depositToDefaultStream(); + + // Make a withdrawal and pay the fee. + flow.withdrawMax{ value: FEE }({ streamId: defaultStreamId, to: users.recipient }); + + resetPrank({ msgSender: users.admin }); + } + + function test_GivenAdminIsNotContract() external { + _test_CollectFees(users.admin); + } + + function test_RevertGiven_AdminDoesNotImplementReceiveFunction() external givenAdminIsContract { + // Transfer the admin to a contract that does not implement the receive function. + flow.transferAdmin(address(contractWithoutReceive)); + + // Make the contract the caller. + resetPrank({ msgSender: address(contractWithoutReceive) }); + + // Expect a revert. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierFlowBase_FeeTransferFail.selector, address(contractWithoutReceive), address(flow).balance + ) + ); + + // Collect the fees. + flow.collectFees(); + } + + function test_GivenAdminImplementsReceiveFunction() external givenAdminIsContract { + // Transfer the admin to a contract that implements the receive function. + flow.transferAdmin(address(contractWithReceive)); + + // Make the contract the caller. + resetPrank({ msgSender: address(contractWithReceive) }); + + // Run the tests. + _test_CollectFees(address(contractWithReceive)); + } + + function _test_CollectFees(address admin) private { + vm.warp({ newTimestamp: WITHDRAW_TIME }); + + // Load the initial ETH balance of the admin. + uint256 initialAdminBalance = admin.balance; + + // It should emit a {CollectFees} event. + vm.expectEmit({ emitter: address(flow) }); + emit ISablierFlowBase.CollectFees({ admin: admin, feeAmount: FEE }); + + flow.collectFees(); + + // It should transfer the fee. + assertEq(admin.balance, initialAdminBalance + FEE, "admin ETH balance"); + + // It should decrease contract balance to zero. + assertEq(address(flow).balance, 0, "flow ETH balance"); + } +} diff --git a/tests/integration/concrete/collect-fees/collectFees.tree b/tests/integration/concrete/collect-fees/collectFees.tree new file mode 100644 index 00000000..a685ba65 --- /dev/null +++ b/tests/integration/concrete/collect-fees/collectFees.tree @@ -0,0 +1,12 @@ +CollectFees_Integration_Concrete_Test +├── given admin is not contract +│ ├── it should transfer fee +│ ├── it should decrease contract balance to zero +│ └── it should emit a {CollectFees} event +└── given admin is contract + ├── given admin does not implement receive function + │ └── it should revert + └── given admin implements receive function + ├── it should transfer fee + ├── it should decrease contract balance to zero + └── it should emit a {CollectFees} event \ No newline at end of file diff --git a/tests/integration/concrete/payable/payable.t.sol b/tests/integration/concrete/payable/payable.t.sol new file mode 100644 index 00000000..3adc60c4 --- /dev/null +++ b/tests/integration/concrete/payable/payable.t.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ud21x18 } from "@prb/math/src/UD21x18.sol"; + +import { Shared_Integration_Concrete_Test } from "../Concrete.t.sol"; + +contract Payable_Integration_Concrete_Test is Shared_Integration_Concrete_Test { + function setUp() public override { + Shared_Integration_Concrete_Test.setUp(); + depositToDefaultStream(); + + vm.warp({ newTimestamp: WARP_ONE_MONTH }); + + // Make the sender the caller. + resetPrank({ msgSender: users.sender }); + } + + function test_AdjustRatePerSecondWhenETHValueNotZero() external { + flow.adjustRatePerSecond{ value: FEE }(defaultStreamId, ud21x18(RATE_PER_SECOND_U128 + 1)); + } + + function test_CreateWhenETHValueNotZero() external { + flow.create{ value: FEE }(users.sender, users.recipient, RATE_PER_SECOND, usdc, TRANSFERABLE); + } + + function test_CreateAndDepositWhenETHValueNotZero() external { + flow.createAndDeposit{ value: FEE }( + users.sender, users.recipient, RATE_PER_SECOND, usdc, TRANSFERABLE, DEPOSIT_AMOUNT_6D + ); + } + + function test_DepositWhenETHValueNotZero() external { + flow.deposit{ value: FEE }(defaultStreamId, DEPOSIT_AMOUNT_6D, users.sender, users.recipient); + } + + function test_DepositAndPauseWhenETHValueNotZero() external { + flow.depositAndPause{ value: FEE }(defaultStreamId, DEPOSIT_AMOUNT_6D); + } + + function test_DepositViaBrokerWhenETHValueNotZero() external { + flow.depositViaBroker{ value: FEE }( + defaultStreamId, DEPOSIT_AMOUNT_6D, users.sender, users.recipient, defaultBroker + ); + } + + function test_PauseWhenETHValueNotZero() external { + flow.pause{ value: FEE }(defaultStreamId); + } + + function test_RefundWhenETHValueNotZero() external { + flow.refund{ value: FEE }(defaultStreamId, REFUND_AMOUNT_6D); + } + + function test_RefundAndPauseWhenETHValueNotZero() external { + flow.refundAndPause{ value: FEE }(defaultStreamId, REFUND_AMOUNT_6D); + } + + function test_RefundMaxWhenETHValueNotZero() external { + flow.refundMax{ value: FEE }(defaultStreamId); + } + + function test_RestartWhenETHValueNotZero() external { + flow.pause(defaultStreamId); + flow.restart{ value: FEE }(defaultStreamId, RATE_PER_SECOND); + } + + function test_RestartAndDepositWhenETHValueNotZero() external { + flow.pause(defaultStreamId); + flow.restartAndDeposit{ value: FEE }(defaultStreamId, RATE_PER_SECOND, DEPOSIT_AMOUNT_6D); + } + + function test_VoidWhenETHValueNotZero() external { + flow.void{ value: FEE }(defaultStreamId); + } + + function test_WithdrawWhenETHValueNotZero() external { + flow.withdraw{ value: FEE }(defaultStreamId, users.recipient, WITHDRAW_AMOUNT_6D); + } + + function test_WithdrawMaxWhenETHValueNotZero() external { + flow.withdrawMax{ value: FEE }(defaultStreamId, users.recipient); + } +} diff --git a/tests/integration/concrete/payable/payable.tree b/tests/integration/concrete/payable/payable.tree new file mode 100644 index 00000000..a27790e5 --- /dev/null +++ b/tests/integration/concrete/payable/payable.tree @@ -0,0 +1,59 @@ +Payable_Integration_Concrete_Test::adjustRatePerSecond +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::create +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::createAndDeposit +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::deposit +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::depositAndPause +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::depositViaBroker +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::pause +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::refund +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::refundAndPause +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::refundMax +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::restart +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::restartAndDeposit +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::void +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::withdraw +└── when ETH value not zero + └── it should make the call + +Payable_Integration_Concrete_Test::withdrawMax +└── when ETH value not zero + └── it should make the call \ No newline at end of file diff --git a/tests/mocks/Receive.sol b/tests/mocks/Receive.sol new file mode 100644 index 00000000..31a8fa96 --- /dev/null +++ b/tests/mocks/Receive.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +contract ContractWithoutReceive { } + +contract ContractWithReceive { + receive() external payable { } +} diff --git a/tests/utils/Constants.sol b/tests/utils/Constants.sol index 35ed05e9..27e18fb2 100644 --- a/tests/utils/Constants.sol +++ b/tests/utils/Constants.sol @@ -8,6 +8,7 @@ abstract contract Constants { // Amounts uint128 internal constant DEPOSIT_AMOUNT_18D = 50_000e18; uint128 internal constant DEPOSIT_AMOUNT_6D = 50_000e6; + uint256 internal constant FEE = 0.001e18; uint128 internal constant REFUND_AMOUNT_18D = 10_000e18; uint128 internal constant REFUND_AMOUNT_6D = 10_000e6; uint128 internal constant TOTAL_AMOUNT_WITH_BROKER_FEE_18D = DEPOSIT_AMOUNT_18D + BROKER_FEE_AMOUNT_18D; diff --git a/tests/utils/Modifiers.sol b/tests/utils/Modifiers.sol index b75679ee..f1757f51 100644 --- a/tests/utils/Modifiers.sol +++ b/tests/utils/Modifiers.sol @@ -64,6 +64,14 @@ abstract contract Modifiers is Utils { _; } + /*////////////////////////////////////////////////////////////////////////// + COLLECT-FEES + //////////////////////////////////////////////////////////////////////////*/ + + modifier givenAdminIsContract() { + _; + } + /*////////////////////////////////////////////////////////////////////////// CREATE //////////////////////////////////////////////////////////////////////////*/