diff --git a/tests/integration/concrete/batch/batch.t.sol b/tests/integration/concrete/batch/batch.t.sol index 4dd9a045e..862f0652c 100644 --- a/tests/integration/concrete/batch/batch.t.sol +++ b/tests/integration/concrete/batch/batch.t.sol @@ -7,18 +7,9 @@ import { Integration_Test } from "../../Integration.t.sol"; contract Batch_Integration_Concrete_Test is Integration_Test { /*////////////////////////////////////////////////////////////////////////// - REVERT + BATCH + LOCKUP //////////////////////////////////////////////////////////////////////////*/ - /// @dev The batch call includes ETH and a non-payable function. - function test_RevertWhen_ETHWithNonPayable() external { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeCall(lockup.isCancelable, (defaultStreamId)); - - vm.expectRevert(); - lockup.batch{ value: 1 wei }(calls); - } - /// @dev The batch call cancels a non-cancelable stream. function test_RevertWhen_LockupThrows() external { bytes[] memory calls = new bytes[](2); @@ -32,29 +23,6 @@ contract Batch_Integration_Concrete_Test is Integration_Test { lockup.batch(calls); } - /// @dev The batch call includes a non-existent function. - function test_RevertWhen_NonExistentFunction() external { - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeWithSignature("nonExistentFunction()"); - - vm.expectRevert(); - lockup.batch(calls); - } - - /// @dev The batch call reverts with a string reason. - function test_RevertWhen_StringReason() external { - bytes[] memory calls = new bytes[](2); - calls[0] = abi.encodeCall(lockup.cancel, (defaultStreamId)); - calls[1] = abi.encodeCall(lockup.cancel, (recipientRevertStreamId)); - - vm.expectRevert("You shall not pass"); - lockup.batch(calls); - } - - /*////////////////////////////////////////////////////////////////////////// - RETURN - //////////////////////////////////////////////////////////////////////////*/ - /// @dev The batch call includes: /// - Returning state changing functions /// - Non-returning state changing functions @@ -90,41 +58,6 @@ contract Batch_Integration_Concrete_Test is Integration_Test { assertEq(results[5], hex""); } - /// @dev The batch call includes: - /// - ETH value - /// - Returning state changing functions - /// - Non-returning state changing functions - function test_BatchPayable_StateChangingFunctions() external { - uint256 expectedNextStreamId = lockup.nextStreamId(); - uint256 initialEthBalance = address(lockup).balance; - vm.warp(defaults.WARP_26_PERCENT()); - - bytes[] memory calls = new bytes[](4); - // It will return the withdrawn amount. - calls[0] = abi.encodeCall(lockup.withdrawMax, (notCancelableStreamId, users.recipient)); - // It will not return anything. - calls[1] = abi.encodeCall(lockup.cancel, (defaultStreamId)); - // It will return the stream ID created. - calls[2] = abi.encodeCall( - lockup.createWithTimestampsLL, - (defaults.createWithTimestamps(), defaults.unlockAmounts(), defaults.CLIFF_TIME()) - ); - // It will not return anything. - calls[3] = abi.encodeCall(lockup.renounce, (notTransferableStreamId)); - - bytes[] memory results = lockup.batch{ value: 1 wei }(calls); - assertEq(results.length, 4); - assertEq(abi.decode(results[0], (uint128)), defaults.WITHDRAW_AMOUNT()); - assertEq(results[1], hex""); - assertEq(abi.decode(results[2], (uint256)), expectedNextStreamId); - assertEq(results[3], hex""); - assertEq(address(lockup).balance, initialEthBalance + 1 wei); - } - - /*////////////////////////////////////////////////////////////////////////// - BATCH + LOCKUP - //////////////////////////////////////////////////////////////////////////*/ - /// @dev The batch call includes: /// - ETH value /// - All create stream functions that return a value diff --git a/tests/mocks/BatchMock.sol b/tests/mocks/BatchMock.sol new file mode 100644 index 000000000..b9d4b4edd --- /dev/null +++ b/tests/mocks/BatchMock.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { Batch } from "src/abstracts/Batch.sol"; + +contract BatchMock is Batch { + error InvalidNumber(uint256); + + uint256 internal _number = 10; + + // A view only function. + function getNumber() public view returns (uint256) { + return _number; + } + + // A view only function that reverts. + function getNumberAndRevert() public pure returns (uint256) { + revert InvalidNumber(1); + } + + // A state changing function with no return value. + function setNumber(uint256 number) public { + _number = number; + } + + // A state changing function with payable modifier and return value. + function setNumberWithPayableAndReturn(uint256 number) public payable returns (uint256) { + _number = number; + return _number; + } + + // A state changing function with payable modifier but reverts with custom error. + function setNumberWithPayableAndRevert(uint256 number) public payable { + _number = number; + revert InvalidNumber(number); + } + + // A state changing function with payable modifier but reverts with string error. + function setNumberWithPayableAndRevertString(uint256 number) public payable { + _number = number; + revert("You cannot pass"); + } + + // A state changing function with payable modifier and no return value. + function setNumberWithPayable(uint256 number) public payable { + _number = number; + } +} diff --git a/tests/unit/concrete/batch/batch.t.sol b/tests/unit/concrete/batch/batch.t.sol new file mode 100644 index 000000000..cb0af504d --- /dev/null +++ b/tests/unit/concrete/batch/batch.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.22; + +import { Base_Test } from "../../../Base.t.sol"; +import { BatchMock } from "../../../mocks/BatchMock.sol"; + +contract Batch_Unit_Concrete_Test is Base_Test { + BatchMock internal batchMock; + bytes[] internal calls; + bytes[] internal results; + uint256 internal newNumber = 100; + + function setUp() public virtual override { + Base_Test.setUp(); + + batchMock = new BatchMock(); + } + + function test_RevertWhen_FunctionDoesNotExist() external { + calls = new bytes[](1); + calls[0] = abi.encodeWithSignature("nonExistentFunction()"); + + // It should revert. + vm.expectRevert(bytes("")); + batchMock.batch(calls); + } + + modifier whenFunctionExists() { + _; + } + + modifier givenNonStateChangingFunction() { + _; + } + + function test_RevertWhen_FunctionReverts() external whenFunctionExists givenNonStateChangingFunction { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.getNumberAndRevert, ()); + + // It should revert. + vm.expectRevert(abi.encodeWithSelector(BatchMock.InvalidNumber.selector, 1)); + batchMock.batch(calls); + } + + function test_WhenFunctionNotRevert() external whenFunctionExists givenNonStateChangingFunction { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.getNumber, ()); + results = batchMock.batch(calls); + + // It should return expected value. + assertEq(results.length, 1); + assertEq(abi.decode(results[0], (uint256)), 10); + } + + modifier givenStateChangingFunction() { + _; + } + + modifier whenNotPayable() { + _; + } + + function test_RevertWhen_BatchIncludesETHValue() + external + whenFunctionExists + givenStateChangingFunction + whenNotPayable + { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.setNumber, (newNumber)); + + // It should revert. + vm.expectRevert(bytes("")); + batchMock.batch{ value: 1 wei }(calls); + } + + function test_WhenBatchNotIncludeETHValue() external whenFunctionExists givenStateChangingFunction whenNotPayable { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.setNumber, (newNumber)); + + results = batchMock.batch(calls); + + // It should return empty value. + assertEq(results.length, 1); + assertEq(results[0], hex""); + } + + modifier whenPayable() { + _; + } + + function test_RevertWhen_FunctionRevertsWithCustomError() + external + whenFunctionExists + givenStateChangingFunction + whenPayable + { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.setNumberWithPayableAndRevert, (newNumber)); + + // It should revert. + vm.expectRevert(abi.encodeWithSelector(BatchMock.InvalidNumber.selector, newNumber)); + batchMock.batch{ value: 1 wei }(calls); + } + + function test_RevertWhen_FunctionRevertsWithStringError() + external + whenFunctionExists + givenStateChangingFunction + whenPayable + { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.setNumberWithPayableAndRevertString, (newNumber)); + + // It should revert. + vm.expectRevert("You cannot pass"); + batchMock.batch{ value: 1 wei }(calls); + } + + function test_WhenFunctionReturnsAValue() external whenFunctionExists givenStateChangingFunction whenPayable { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.setNumberWithPayableAndReturn, (newNumber)); + results = batchMock.batch{ value: 1 wei }(calls); + + // It should return expected value. + assertEq(results.length, 1); + assertEq(abi.decode(results[0], (uint256)), newNumber); + } + + function test_WhenFunctionDoesNotReturnAValue() + external + whenFunctionExists + givenStateChangingFunction + whenPayable + { + calls = new bytes[](1); + calls[0] = abi.encodeCall(batchMock.setNumberWithPayable, (newNumber)); + results = batchMock.batch{ value: 1 wei }(calls); + + // It should return empty value. + assertEq(results.length, 1); + assertEq(results[0], hex""); + } +} diff --git a/tests/unit/concrete/batch/batch.tree b/tests/unit/concrete/batch/batch.tree new file mode 100644 index 000000000..763be37a8 --- /dev/null +++ b/tests/unit/concrete/batch/batch.tree @@ -0,0 +1,24 @@ +Batch_Unit_Concrete_Test +├── when function does not exist +│ └── it should revert +└── when function exists + ├── given non state changing function + │ ├── when function reverts + │ │ └── it should revert + │ └── when function not revert + │ └── it should return expected value + └── given state changing function + ├── when not payable + │ ├── when batch includes ETH value + │ │ └── it should revert + │ └── when batch not include ETH value + │ └── it should return empty value + └── when payable + ├── when function reverts with custom error + │ └── it should revert + ├── when function reverts with string error + │ └── it should revert + ├── when function returns a value + │ └── it should return expected value + └── when function does not return a value + └── it should return empty value