diff --git a/bun.lockb b/bun.lockb index 4ab3611c..11e0967c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/remappings.txt b/remappings.txt index 89f80792..4f4f01a0 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,4 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ @prb/math/=node_modules/@prb/math/ -forge-std/=node_modules/forge-std/ -solady/=node_modules/solady/ \ No newline at end of file +forge-std/=node_modules/forge-std/ +solady/=node_modules/solady/ diff --git a/src/abstracts/Batch.sol b/src/abstracts/Batch.sol index d6d2f7e0..129d108a 100644 --- a/src/abstracts/Batch.sol +++ b/src/abstracts/Batch.sol @@ -1,26 +1,39 @@ // SPDX-License-Identifier: GPL-3.0-or-later +// solhint-disable no-inline-assembly pragma solidity >=0.8.22; import { IBatch } from "../interfaces/IBatch.sol"; -import { Errors } from "../libraries/Errors.sol"; /// @title Batch /// @notice See the documentation in {IBatch}. -/// @dev Forked from: https://github.com/boringcrypto/BoringSolidity/blob/master/contracts/BoringBatchable.sol abstract contract Batch is IBatch { /*////////////////////////////////////////////////////////////////////////// USER-FACING NON-CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IBatch - function batch(bytes[] calldata calls) external payable override { + /// @dev Since `msg.value` can be reused across calls, be VERY CAREFUL when using it. Refer to + /// https://paradigm.xyz/2021/08/two-rights-might-make-a-wrong for more information. + function batch(bytes[] calldata calls) external payable override returns (bytes[] memory results) { uint256 count = calls.length; + results = new bytes[](count); for (uint256 i = 0; i < count; ++i) { (bool success, bytes memory result) = address(this).delegatecall(calls[i]); + + // Check: If the delegatecall failed, load and bubble up the revert data. if (!success) { - revert Errors.BatchError(result); + assembly { + // Get the length of the result stored in the first 32 bytes. + let resultSize := mload(result) + + // Forward the pointer by 32 bytes to skip the length argument, and revert with the result. + revert(add(32, result), resultSize) + } } + + // Push the result into the results array. + results[i] = result; } } } diff --git a/src/interfaces/IBatch.sol b/src/interfaces/IBatch.sol index 862762ef..dd6de1ac 100644 --- a/src/interfaces/IBatch.sol +++ b/src/interfaces/IBatch.sol @@ -3,7 +3,10 @@ pragma solidity >=0.8.22; /// @notice This contract implements logic to batch call any function. interface IBatch { - /// @notice Allows batched call to self, `this` contract. + /// @notice Allows batched calls to self, i.e., `this` contract. + /// @dev Since `msg.value` can be reused across calls, be VERY CAREFUL when using it. Refer to + /// https://paradigm.xyz/2021/08/two-rights-might-make-a-wrong for more information. /// @param calls An array of inputs for each call. - function batch(bytes[] calldata calls) external payable; + /// @return results An array of results from each call. Empty when the calls do not return anything. + function batch(bytes[] calldata calls) external payable returns (bytes[] memory results); } diff --git a/tests/fork/Fork.t.sol b/tests/fork/Fork.t.sol index 20b050ec..6b390d15 100644 --- a/tests/fork/Fork.t.sol +++ b/tests/fork/Fork.t.sol @@ -3,7 +3,6 @@ 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"; diff --git a/tests/integration/concrete/batch/batch.t.sol b/tests/integration/concrete/batch/batch.t.sol index 14e63c55..4994cd60 100644 --- a/tests/integration/concrete/batch/batch.t.sol +++ b/tests/integration/concrete/batch/batch.t.sol @@ -2,7 +2,6 @@ pragma solidity >=0.8.22; import { IERC4906 } from "@openzeppelin/contracts/interfaces/IERC4906.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { ud21x18, UD21x18 } from "@prb/math/src/UD21x18.sol"; import { ISablierFlow } from "src/interfaces/ISablierFlow.sol"; import { Errors } from "src/libraries/Errors.sol"; @@ -13,67 +12,25 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { function setUp() public override { Shared_Integration_Concrete_Test.setUp(); - defaultStreamIds.push(defaultStreamId); - // Create a second stream - vm.warp({ newTimestamp: getBlockTimestamp() - ONE_MONTH }); + // The first stream is the default stream. + defaultStreamIds.push(defaultStreamId); + // Create a new stream as the second stream. defaultStreamIds.push(createDefaultStream()); - - vm.warp({ newTimestamp: WARP_ONE_MONTH }); } /*////////////////////////////////////////////////////////////////////////// REVERT //////////////////////////////////////////////////////////////////////////*/ - function test_RevertWhen_CustomError() external { - // The calls declared as bytes. - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeCall(flow.withdrawMax, (1, users.recipient)); - - bytes memory expectedRevertData = abi.encodeWithSelector( - Errors.BatchError.selector, abi.encodeWithSelector(Errors.SablierFlow_WithdrawAmountZero.selector, 1) - ); - - vm.expectRevert(expectedRevertData); - flow.batch(calls); - } - - function test_RevertWhen_StringMessage() external { - uint256 streamId = flow.create({ - sender: users.sender, - recipient: users.recipient, - ratePerSecond: RATE_PER_SECOND, - token: IERC20(address(usdt)), - transferable: TRANSFERABLE - }); - - address noAllowanceAddress = address(0xBEEF); - resetPrank({ msgSender: noAllowanceAddress }); - - // The calls declared as bytes. - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeCall(flow.deposit, (streamId, DEPOSIT_AMOUNT_6D, users.sender, users.recipient)); - - bytes memory expectedRevertData = abi.encodeWithSelector( - Errors.BatchError.selector, abi.encodeWithSignature("Error(string)", "ERC20: insufficient allowance") - ); - - vm.expectRevert(expectedRevertData); - flow.batch(calls); - } - - function test_RevertWhen_SilentRevert() external { - uint256 streamId = createDefaultStream(IERC20(address(usdt))); - - // The calls declared as bytes - bytes[] memory calls = new bytes[](1); - calls[0] = abi.encodeCall(flow.refund, (streamId, REFUND_AMOUNT_6D)); - - // Remove the ERC-20 balance from flow contract. - deal({ token: address(usdt), to: address(flow), give: 0 }); + /// @dev The batch call pauses a null stream. + function test_RevertWhen_FlowThrows() external { + bytes[] memory calls = new bytes[](2); + calls[0] = abi.encodeCall(flow.pause, (defaultStreamId)); + calls[1] = abi.encodeCall(flow.pause, (nullStreamId)); - vm.expectRevert(); + // It should revert on nullStreamId. + vm.expectRevert(abi.encodeWithSelector(Errors.SablierFlow_Null.selector, nullStreamId)); flow.batch(calls); } @@ -82,18 +39,13 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { //////////////////////////////////////////////////////////////////////////*/ function test_Batch_AdjustRatePerSecond() external { - depositDefaultAmount(defaultStreamIds[0]); - depositDefaultAmount(defaultStreamIds[1]); - UD21x18 newRatePerSecond = ud21x18(RATE_PER_SECOND.unwrap() + 1); bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(flow.adjustRatePerSecond, (defaultStreamIds[0], newRatePerSecond)); calls[1] = abi.encodeCall(flow.adjustRatePerSecond, (defaultStreamIds[1], newRatePerSecond)); - // It should emit 2 {AdjustRatePerSecond} and 2 {MetadataUpdate} events. - - // First stream to adjust rate per second + // It should emit 2 {AdjustRatePerSecond} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.AdjustFlowStream({ streamId: defaultStreamIds[0], @@ -101,23 +53,14 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { oldRatePerSecond: RATE_PER_SECOND, newRatePerSecond: newRatePerSecond }); - - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[0] }); - - // Second stream to adjust rate per second vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.AdjustFlowStream({ streamId: defaultStreamIds[1], - totalDebt: ONE_MONTH_DEBT_6D, + totalDebt: 0, oldRatePerSecond: RATE_PER_SECOND, newRatePerSecond: newRatePerSecond }); - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[1] }); - - // Call the batch function. flow.batch(calls); } @@ -126,47 +69,17 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { //////////////////////////////////////////////////////////////////////////*/ function test_Batch_Create() external { - uint256[] memory expectedStreamIds = new uint256[](2); - expectedStreamIds[0] = flow.nextStreamId(); - expectedStreamIds[1] = flow.nextStreamId() + 1; + uint256 expectedNextStreamId = flow.nextStreamId(); - // The calls declared as bytes. bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(flow.create, (users.sender, users.recipient, RATE_PER_SECOND, usdc, TRANSFERABLE)); calls[1] = abi.encodeCall(flow.create, (users.sender, users.recipient, RATE_PER_SECOND, usdc, TRANSFERABLE)); - // It should emit 2 {MetadataUpdate} and 2 {CreateFlowStream} events. - - // First stream to create. - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamIds[0] }); - - vm.expectEmit({ emitter: address(flow) }); - emit ISablierFlow.CreateFlowStream({ - streamId: expectedStreamIds[0], - sender: users.sender, - recipient: users.recipient, - ratePerSecond: RATE_PER_SECOND, - token: usdc, - transferable: TRANSFERABLE - }); - - // Second stream to create. - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: expectedStreamIds[1] }); - - vm.expectEmit({ emitter: address(flow) }); - emit ISablierFlow.CreateFlowStream({ - streamId: expectedStreamIds[1], - sender: users.sender, - recipient: users.recipient, - ratePerSecond: RATE_PER_SECOND, - token: usdc, - transferable: TRANSFERABLE - }); - // Call the batch function. - flow.batch(calls); + bytes[] memory results = flow.batch(calls); + assertEq(results.length, 2, "batch results length"); + assertEq(abi.decode(results[0], (uint256)), expectedNextStreamId, "batch results[0]"); + assertEq(abi.decode(results[1], (uint256)), expectedNextStreamId + 1, "batch results[1]"); } /*////////////////////////////////////////////////////////////////////////// @@ -174,31 +87,17 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { //////////////////////////////////////////////////////////////////////////*/ function test_Batch_Deposit() external { - // The calls declared as bytes. bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(flow.deposit, (defaultStreamIds[0], DEPOSIT_AMOUNT_6D, users.sender, users.recipient)); calls[1] = abi.encodeCall(flow.deposit, (defaultStreamIds[1], DEPOSIT_AMOUNT_6D, users.sender, users.recipient)); - // It should emit 2 {Transfer}, 2 {DepositFlowStream}, 2 {MetadataUpdate} events. - - // First stream to deposit. - vm.expectEmit({ emitter: address(usdc) }); - emit IERC20.Transfer({ from: users.sender, to: address(flow), value: DEPOSIT_AMOUNT_6D }); - + // It should emit 2 {DepositFlowStream} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.DepositFlowStream({ streamId: defaultStreamIds[0], funder: users.sender, amount: DEPOSIT_AMOUNT_6D }); - - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[0] }); - - // Second stream to deposit. - vm.expectEmit({ emitter: address(usdc) }); - emit IERC20.Transfer({ from: users.sender, to: address(flow), value: DEPOSIT_AMOUNT_6D }); - vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.DepositFlowStream({ streamId: defaultStreamIds[1], @@ -206,9 +105,6 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { amount: DEPOSIT_AMOUNT_6D }); - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[1] }); - // It should perform the ERC-20 transfers. expectCallToTransferFrom({ token: usdc, from: users.sender, to: address(flow), amount: DEPOSIT_AMOUNT_6D }); expectCallToTransferFrom({ token: usdc, from: users.sender, to: address(flow), amount: DEPOSIT_AMOUNT_6D }); @@ -222,40 +118,26 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { //////////////////////////////////////////////////////////////////////////*/ function test_Batch_Pause() external { - // The calls declared as bytes. bytes[] memory calls = new bytes[](2); calls[0] = abi.encodeCall(flow.pause, (defaultStreamIds[0])); calls[1] = abi.encodeCall(flow.pause, (defaultStreamIds[1])); - uint256 previousTotalDebt0 = flow.totalDebtOf(defaultStreamId); - uint256 previousTotalDebt1 = flow.totalDebtOf(defaultStreamIds[1]); - - // It should emit 2 {PauseFlowStream} and 2 {MetadataUpdate} events. - - // First stream pause. + // It should emit 2 {PauseFlowStream} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.PauseFlowStream({ streamId: defaultStreamIds[0], recipient: users.recipient, sender: users.sender, - totalDebt: previousTotalDebt0 + totalDebt: ONE_MONTH_DEBT_6D }); - - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[0] }); - - // Second stream pause. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.PauseFlowStream({ streamId: defaultStreamIds[1], sender: users.sender, recipient: users.recipient, - totalDebt: previousTotalDebt1 + totalDebt: 0 }); - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[1] }); - // Call the batch function. flow.batch(calls); } @@ -273,23 +155,13 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { calls[0] = abi.encodeCall(flow.refund, (defaultStreamIds[0], REFUND_AMOUNT_6D)); calls[1] = abi.encodeCall(flow.refund, (defaultStreamIds[1], REFUND_AMOUNT_6D)); - // It should emit 2 {Transfer} and 2 {RefundFromFlowStream} events. - - // First stream refund. - vm.expectEmit({ emitter: address(usdc) }); - emit IERC20.Transfer({ from: address(flow), to: users.sender, value: REFUND_AMOUNT_6D }); - + // It should emit 2 {RefundFromFlowStream} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.RefundFromFlowStream({ streamId: defaultStreamIds[0], sender: users.sender, amount: REFUND_AMOUNT_6D }); - - // Second stream refund. - vm.expectEmit({ emitter: address(usdc) }); - emit IERC20.Transfer({ from: address(flow), to: users.sender, value: REFUND_AMOUNT_6D }); - vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.RefundFromFlowStream({ streamId: defaultStreamIds[1], @@ -318,20 +190,13 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { calls[0] = abi.encodeCall(flow.restart, (defaultStreamIds[0], RATE_PER_SECOND)); calls[1] = abi.encodeCall(flow.restart, (defaultStreamIds[1], RATE_PER_SECOND)); - // It should emit 2 {RestartFlowStream} and 2 {MetadataUpdate} events. - - // First stream restart. + // It should emit 2 {RestartFlowStream} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.RestartFlowStream({ streamId: defaultStreamIds[0], sender: users.sender, ratePerSecond: RATE_PER_SECOND }); - - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[0] }); - - // Second stream restart. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.RestartFlowStream({ streamId: defaultStreamIds[1], @@ -339,9 +204,6 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { ratePerSecond: RATE_PER_SECOND }); - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[1] }); - // Call the batch function. flow.batch(calls); } @@ -351,6 +213,9 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { //////////////////////////////////////////////////////////////////////////*/ function test_Batch_Withdraw() external { + // Warp to one more month so that the second stream has also accrued some debt. + vm.warp({ newTimestamp: getBlockTimestamp() + ONE_MONTH }); + depositDefaultAmount(defaultStreamIds[0]); depositDefaultAmount(defaultStreamIds[1]); @@ -359,12 +224,7 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { calls[0] = abi.encodeCall(flow.withdraw, (defaultStreamIds[0], users.recipient, WITHDRAW_AMOUNT_6D)); calls[1] = abi.encodeCall(flow.withdraw, (defaultStreamIds[1], users.recipient, WITHDRAW_AMOUNT_6D)); - // It should emit 2 {Transfer}, 2 {WithdrawFromFlowStream} and 2 {MetadataUpdated} events. - - // First stream withdrawal. - vm.expectEmit({ emitter: address(usdc) }); - emit IERC20.Transfer({ from: address(flow), to: users.recipient, value: WITHDRAW_AMOUNT_6D }); - + // It should emit 2 {WithdrawFromFlowStream} events. vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.WithdrawFromFlowStream({ streamId: defaultStreamIds[0], @@ -374,14 +234,6 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { protocolFeeAmount: 0, withdrawAmount: WITHDRAW_AMOUNT_6D }); - - vm.expectEmit({ emitter: address(flow) }); - emit IERC4906.MetadataUpdate({ _tokenId: defaultStreamIds[0] }); - - // Second stream withdrawal. - vm.expectEmit({ emitter: address(usdc) }); - emit IERC20.Transfer({ from: address(flow), to: users.recipient, value: WITHDRAW_AMOUNT_6D }); - vm.expectEmit({ emitter: address(flow) }); emit ISablierFlow.WithdrawFromFlowStream({ streamId: defaultStreamIds[1], @@ -400,6 +252,13 @@ contract Batch_Integration_Concrete_Test is Shared_Integration_Concrete_Test { expectCallToTransfer({ token: usdc, to: users.recipient, amount: WITHDRAW_AMOUNT_6D }); // Call the batch function. - flow.batch(calls); + bytes[] memory results = flow.batch(calls); + assertEq(results.length, 2, "batch results length"); + (uint128 actualWithdrawnAmount, uint128 actualProtocolFeeAmount) = abi.decode(results[0], (uint128, uint128)); + assertEq(actualWithdrawnAmount, WITHDRAW_AMOUNT_6D, "batch results[0]"); + assertEq(actualProtocolFeeAmount, 0, "batch results[0]"); + (actualWithdrawnAmount, actualProtocolFeeAmount) = abi.decode(results[1], (uint128, uint128)); + assertEq(actualWithdrawnAmount, WITHDRAW_AMOUNT_6D, "batch results[1]"); + assertEq(actualProtocolFeeAmount, 0, "batch results[1]"); } }