Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: withdraw multiple to recipient #216

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/abstracts/SablierV2ProxyTarget.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,39 @@ abstract contract SablierV2ProxyTarget is
lockup.withdrawMultiple(streamIds, to, amounts);
}

/// @inheritdoc ISablierV2ProxyTarget
function withdrawMultipleToRecipient(
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
ISablierV2Lockup lockup,
uint256[] calldata streamIds,
uint128[] calldata amounts
)
external
onlyDelegateCall
{
// Checks: there is an equal number of `streamIds` and `amounts`.
uint256 streamIdsCount = streamIds.length;
uint256 amountsCount = amounts.length;
if (streamIdsCount != amountsCount) {
revert Errors.SablierV2ProxyTarget_WithdrawArrayCountsNotEqual(streamIdsCount, amountsCount);
}

address to;

// Iterate over the provided array of stream ids and withdraw from each stream.
for (uint256 i = 0; i < streamIdsCount;) {
// Retrieve the recipient of the stream.
to = lockup.getRecipient(streamIds[i]);

// Checks, Effects and Interactions: check the parameters and make the withdrawal.
lockup.withdraw(streamIds[i], to, amounts[i]);

// Increment the loop iterator.
unchecked {
i += 1;
}
}
}

/*//////////////////////////////////////////////////////////////////////////
SABLIER-V2-LOCKUP-LINEAR
//////////////////////////////////////////////////////////////////////////*/
Expand Down
15 changes: 15 additions & 0 deletions src/interfaces/ISablierV2ProxyTarget.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ interface ISablierV2ProxyTarget {
)
external;

/// @notice Withdraws assets from streams to the recipient.
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
///
/// @dev Notes:
/// - Retrieves the recipient of each stream from {ISablierV2Lockup.getRecipient}.
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
///
/// Requirements:
/// - Must be delegate called.
///
function withdrawMultipleToRecipient(
ISablierV2Lockup lockup,
uint256[] calldata streamIds,
uint128[] calldata amounts
)
external;

/*//////////////////////////////////////////////////////////////////////////
SABLIER-V2-LOCKUP-LINEAR
//////////////////////////////////////////////////////////////////////////*/
Expand Down
4 changes: 4 additions & 0 deletions src/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ library Errors {

/// @notice Thrown when trying to wrap and create a stream and the credit amount is not equal to `msg.value`.
error SablierV2ProxyTarget_CreditAmountMismatch(uint256 msgValue, uint256 creditAmount);

/// @notice Thrown when trying to withdraw from multiple streams and the number of stream ids does
/// not match the number of withdraw amounts.
error SablierV2ProxyTarget_WithdrawArrayCountsNotEqual(uint256 streamIdsCount, uint256 amountsCount);
}
16 changes: 16 additions & 0 deletions test/integration/target/TargetApprove.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { Withdraw_Integration_Test } from "./withdraw/withdraw.t.sol";
import { WithdrawMax_Integration_Test } from "./withdraw-max/withdrawMax.t.sol";
import { WithdrawMaxAndTransfer_Integration_Test } from "./withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol";
import { WithdrawMultiple_Integration_Test } from "./withdraw-multiple/withdrawMultiple.t.sol";
import { WithdrawMultipleToRecipient_Integration_Test } from
"./withdraw-multiple-to-recipient/withdrawMultipleToRecipient.t.sol";
import { WrapAndCreate_Integration_Test } from "./wrap-and-create/wrapAndCreate.t.sol";

abstract contract TargetApprove_Integration_Test is Integration_Test {
Expand Down Expand Up @@ -161,6 +163,20 @@ contract WithdrawMultiple_TargetApprove_Integration_Test is
}
}

contract WithdrawMultipleToRecipient_TargetApprove_Integration_Test is
TargetApprove_Integration_Test,
WithdrawMultipleToRecipient_Integration_Test
{
function setUp()
public
virtual
override(TargetApprove_Integration_Test, WithdrawMultipleToRecipient_Integration_Test)
{
TargetApprove_Integration_Test.setUp();
WithdrawMultipleToRecipient_Integration_Test.setUp();
}
}

contract WrapAndCreate_TargetApprove_Integration_Test is
TargetApprove_Integration_Test,
WrapAndCreate_Integration_Test
Expand Down
16 changes: 16 additions & 0 deletions test/integration/target/TargetPermit2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { Withdraw_Integration_Test } from "./withdraw/withdraw.t.sol";
import { WithdrawMax_Integration_Test } from "./withdraw-max/withdrawMax.t.sol";
import { WithdrawMaxAndTransfer_Integration_Test } from "./withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol";
import { WithdrawMultiple_Integration_Test } from "./withdraw-multiple/withdrawMultiple.t.sol";
import { WithdrawMultipleToRecipient_Integration_Test } from
"./withdraw-multiple-to-recipient/withdrawMultipleToRecipient.t.sol";
import { WrapAndCreate_Integration_Test } from "./wrap-and-create/wrapAndCreate.t.sol";

abstract contract TargetPermit2_Integration_Test is Integration_Test {
Expand Down Expand Up @@ -161,6 +163,20 @@ contract WithdrawMultiple_TargetPermit2_Integration_Test is
}
}

contract WithdrawMultipleToRecipient_TargetPermit2_Integration_Test is
TargetPermit2_Integration_Test,
WithdrawMultipleToRecipient_Integration_Test
{
function setUp()
public
virtual
override(TargetPermit2_Integration_Test, WithdrawMultipleToRecipient_Integration_Test)
{
TargetPermit2_Integration_Test.setUp();
WithdrawMultipleToRecipient_Integration_Test.setUp();
}
}

contract WrapAndCreate_TargetPermit2_Integration_Test is
TargetPermit2_Integration_Test,
WrapAndCreate_Integration_Test
Expand Down
16 changes: 16 additions & 0 deletions test/integration/target/TargetPush.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { Withdraw_Integration_Test } from "./withdraw/withdraw.t.sol";
import { WithdrawMax_Integration_Test } from "./withdraw-max/withdrawMax.t.sol";
import { WithdrawMaxAndTransfer_Integration_Test } from "./withdraw-max-and-transfer/withdrawMaxAndTransfer.t.sol";
import { WithdrawMultiple_Integration_Test } from "./withdraw-multiple/withdrawMultiple.t.sol";
import { WithdrawMultipleToRecipient_Integration_Test } from
"./withdraw-multiple-to-recipient/withdrawMultipleToRecipient.t.sol";
import { WrapAndCreate_Integration_Test } from "./wrap-and-create/wrapAndCreate.t.sol";

abstract contract TargetPush_Integration_Test is Integration_Test {
Expand Down Expand Up @@ -151,6 +153,20 @@ contract WithdrawMultiple_TargetPush_Integration_Test is
}
}

contract WithdrawMultipleToRecipient_TargetPush_Integration_Test is
TargetPush_Integration_Test,
WithdrawMultipleToRecipient_Integration_Test
{
function setUp()
public
virtual
override(TargetPush_Integration_Test, WithdrawMultipleToRecipient_Integration_Test)
{
TargetPush_Integration_Test.setUp();
WithdrawMultipleToRecipient_Integration_Test.setUp();
}
}

contract WrapAndCreate_TargetPush_Integration_Test is TargetPush_Integration_Test, WrapAndCreate_Integration_Test {
function setUp() public virtual override(TargetPush_Integration_Test, WrapAndCreate_Integration_Test) {
TargetPush_Integration_Test.setUp();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <0.9.0;

import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol";

import { Errors } from "src/libraries/Errors.sol";

import { Target_Integration_Test } from "../Target.t.sol";

abstract contract WithdrawMultipleToRecipient_Integration_Test is Target_Integration_Test {
andreivladbrg marked this conversation as resolved.
Show resolved Hide resolved
function setUp() public virtual override { }

function test_RevertWhen_NotDelegateCalled() external {
uint256[] memory streamIds;
uint128[] memory amounts;
vm.expectRevert(Errors.CallNotDelegateCall.selector);
target.withdrawMultipleToRecipient(lockupLinear, streamIds, amounts);
}

modifier whenDelegateCalled() {
_;
}

function test_RevertWhen_ArrayCountsNotEqual() external whenDelegateCalled {
uint256[] memory streamIds = new uint256[](2);
uint128[] memory amounts = new uint128[](1);
vm.expectRevert(
abi.encodeWithSelector(
Errors.SablierV2ProxyTarget_WithdrawArrayCountsNotEqual.selector, streamIds.length, amounts.length
)
);
bytes memory data = abi.encodeCall(target.withdrawMultipleToRecipient, (lockupLinear, streamIds, amounts));
aliceProxy.execute(address(target), data);
}

modifier whenArrayCountsAreEqual() {
_;
}

function test_WithdrawMultipleToRecipient_ArrayCountsZero() external whenDelegateCalled whenArrayCountsAreEqual {
uint256[] memory streamIds = new uint256[](0);
uint128[] memory amounts = new uint128[](0);
bytes memory data = abi.encodeCall(target.withdrawMultipleToRecipient, (lockupLinear, streamIds, amounts));
aliceProxy.execute(address(target), data);
}

modifier whenArrayCountsNotZero() {
_;
}

function test_WithdrawMultipleToRecipient_LockupDynamic()
external
whenDelegateCalled
whenArrayCountsAreEqual
whenArrayCountsNotZero
{
uint256[] memory streamIds = batchCreateWithMilestones();
test_WithdrawMultipleToRecipient(lockupDynamic, streamIds);
}

function test_WithdrawMultipleToRecipient_LockupLinear()
external
whenDelegateCalled
whenArrayCountsAreEqual
whenArrayCountsNotZero
{
uint256[] memory streamIds = batchCreateWithRange();
test_WithdrawMultipleToRecipient(lockupLinear, streamIds);
}

function test_WithdrawMultipleToRecipient(ISablierV2Lockup lockup, uint256[] memory streamIds) internal {
// Simulate the passage of time.
vm.warp(defaults.CLIFF_TIME());

uint40 batchSize = uint40(defaults.BATCH_SIZE());
uint128 withdrawAmount = defaults.WITHDRAW_AMOUNT();

// Asset flow: Sablier → recipient
expectMultipleCallsToTransfer({ count: batchSize, to: users.recipient0.addr, amount: withdrawAmount });

uint128[] memory amounts = new uint128[](batchSize);
for (uint256 i = 0; i < batchSize; ++i) {
amounts[i] = withdrawAmount;
}

bytes memory data = abi.encodeCall(target.withdrawMultipleToRecipient, (lockup, streamIds, amounts));
aliceProxy.execute(address(target), data);

// Assert that the withdrawn amount has been updated for all streams.
for (uint256 i = 0; i < batchSize; ++i) {
assertEq(lockup.getWithdrawnAmount(streamIds[i]), withdrawAmount, "withdrawnAmount");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
withdrawMultipleToRecipient.t.sol
├── when not delegate called
│ └── it should revert
└── when delegate called
├── when the input array counts are not equal
│ └── it should revert
└── when the input array counts are equal
└── it should withdraw from multiple streams to recipients
Loading