diff --git a/src/SablierFees.sol b/src/SablierFees.sol index b50d29c..a6967f3 100644 --- a/src/SablierFees.sol +++ b/src/SablierFees.sol @@ -9,6 +9,8 @@ import { Adminable } from "./Adminable.sol"; /// @title SablierFees /// @notice See the documentation in {ISablierFees}. abstract contract SablierFees is Adminable, ISablierFees { + constructor(address initialAdmin) Adminable(initialAdmin) { } + /// @inheritdoc ISablierFees function collectFees() external override { uint256 feeAmount = address(this).balance; diff --git a/tests/mocks/Receive.sol b/tests/mocks/Receive.sol new file mode 100644 index 0000000..31a8fa9 --- /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/mocks/SablierFeesMock.sol b/tests/mocks/SablierFeesMock.sol new file mode 100644 index 0000000..1c31250 --- /dev/null +++ b/tests/mocks/SablierFeesMock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { SablierFees } from "src/SablierFees.sol"; + +contract SablierFeesMock is SablierFees { + constructor(address initialAdmin) SablierFees(initialAdmin) { } +} diff --git a/tests/unit/Unit.t.sol b/tests/unit/Unit.t.sol new file mode 100644 index 0000000..3d4fc14 --- /dev/null +++ b/tests/unit/Unit.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { CommonBase } from "../Base.t.sol"; +import { ContractWithoutReceive, ContractWithReceive } from "../mocks/Receive.sol"; + +abstract contract Unit_Test is CommonBase { + address internal admin; + address internal eve; + + ContractWithoutReceive internal contractWithoutReceive; + ContractWithReceive internal contractWithReceive; + + function setUp() public virtual override { + CommonBase.setUp(); + + admin = createUser("admin"); + eve = createUser("eve"); + contractWithoutReceive = new ContractWithoutReceive(); + contractWithReceive = new ContractWithReceive(); + + resetPrank(admin); + } +} diff --git a/tests/unit/sablier-fees/collectFees.t.sol b/tests/unit/sablier-fees/collectFees.t.sol new file mode 100644 index 0000000..73b9670 --- /dev/null +++ b/tests/unit/sablier-fees/collectFees.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22 <0.9.0; + +import { ISablierFees } from "src/interfaces/ISablierFees.sol"; +import { Errors } from "src/libraries/Errors.sol"; + +import { Unit_Test } from "../Unit.t.sol"; +import { SablierFeesMock } from "../../mocks/SablierFeesMock.sol"; + +contract CollectFees_Unit_Concrete_Test is Unit_Test { + SablierFeesMock internal sablierFeesMock; + + function setUp() public virtual override { + Unit_Test.setUp(); + + sablierFeesMock = new SablierFeesMock(admin); + } + + function test_GivenAdminIsNotContract() external { + _test_CollectFees(admin); + } + + modifier givenAdminIsContract() { + _; + } + + function test_RevertGiven_AdminDoesNotImplementReceiveFunction() external givenAdminIsContract { + // Transfer the admin to a contract that does not implement the receive function. + resetPrank({ msgSender: admin }); + sablierFeesMock.transferAdmin(address(contractWithoutReceive)); + + // Make the contract the caller. + resetPrank({ msgSender: address(contractWithoutReceive) }); + + // Expect a revert. + vm.expectRevert( + abi.encodeWithSelector( + Errors.SablierFees_FeeTransferFail.selector, + address(contractWithoutReceive), + address(sablierFeesMock).balance + ) + ); + + // Collect the fees. + sablierFeesMock.collectFees(); + } + + function test_GivenAdminImplementsReceiveFunction() external givenAdminIsContract { + // Transfer the admin to a contract that implements the receive function. + resetPrank({ msgSender: admin }); + sablierFeesMock.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 { + uint256 fee = 1 ether; + + // Set the contract balance to 1 ETH. + vm.deal({ account: address(sablierFeesMock), newBalance: 1 ether }); + + // Load the initial ETH balance of the admin. + uint256 initialAdminBalance = admin.balance; + + // It should emit a {CollectFees} event. + vm.expectEmit({ emitter: address(sablierFeesMock) }); + emit ISablierFees.CollectFees({ admin: admin, feeAmount: fee }); + + sablierFeesMock.collectFees(); + + // It should transfer the fee. + assertEq(admin.balance, initialAdminBalance + fee, "admin ETH balance"); + + // It should decrease contract balance to zero. + assertEq(address(sablierFeesMock).balance, 0, "sablierFeesMock ETH balance"); + } +} diff --git a/tests/unit/sablier-fees/collectFees.tree b/tests/unit/sablier-fees/collectFees.tree new file mode 100644 index 0000000..bbfec9f --- /dev/null +++ b/tests/unit/sablier-fees/collectFees.tree @@ -0,0 +1,12 @@ +CollectFees_Unit_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