Skip to content

Commit

Permalink
Merge pull request #300 from sablier-labs/feat/add-tranched-batches
Browse files Browse the repository at this point in the history
Feat/add tranched batches
  • Loading branch information
andreivladbrg authored Mar 11, 2024
2 parents 2282afc + f0f7b36 commit 65a51db
Show file tree
Hide file tree
Showing 20 changed files with 560 additions and 58 deletions.
Binary file modified bun.lockb
Binary file not shown.
9 changes: 3 additions & 6 deletions script/DeployProtocol.s.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22 <0.9.0;

import { SablierV2Comptroller } from "@sablier/v2-core/src/SablierV2Comptroller.sol";
import { SablierV2LockupDynamic } from "@sablier/v2-core/src/SablierV2LockupDynamic.sol";
import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol";
import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol";
Expand All @@ -21,7 +20,6 @@ contract DeployProtocol is BaseScript {
virtual
broadcast
returns (
SablierV2Comptroller comptroller,
SablierV2LockupDynamic lockupDynamic,
SablierV2LockupLinear lockupLinear,
SablierV2LockupTranched lockupTranched,
Expand All @@ -31,11 +29,10 @@ contract DeployProtocol is BaseScript {
)
{
// Deploy V2 Core.
comptroller = new SablierV2Comptroller(initialAdmin);
nftDescriptor = new SablierV2NFTDescriptor();
lockupDynamic = new SablierV2LockupDynamic(initialAdmin, comptroller, nftDescriptor, maxCount);
lockupLinear = new SablierV2LockupLinear(initialAdmin, comptroller, nftDescriptor);
lockupTranched = new SablierV2LockupTranched(initialAdmin, comptroller, nftDescriptor, maxCount);
lockupDynamic = new SablierV2LockupDynamic(initialAdmin, nftDescriptor, maxCount);
lockupLinear = new SablierV2LockupLinear(initialAdmin, nftDescriptor);
lockupTranched = new SablierV2LockupTranched(initialAdmin, nftDescriptor, maxCount);

batch = new SablierV2Batch();
merkleLockupFactory = new SablierV2MerkleLockupFactory();
Expand Down
106 changes: 104 additions & 2 deletions src/SablierV2Batch.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ pragma solidity >=0.8.22;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol";
import { LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";
import { LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol";

import { ISablierV2Batch } from "./interfaces/ISablierV2Batch.sol";
import { Errors } from "./libraries/Errors.sol";
Expand Down Expand Up @@ -217,6 +218,107 @@ contract SablierV2Batch is ISablierV2Batch {
}
}

/*//////////////////////////////////////////////////////////////////////////
SABLIER-V2-LOCKUP-TRANCHED
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2Batch
function createWithDurationsLT(
ISablierV2LockupTranched lockupTranched,
IERC20 asset,
Batch.CreateWithDurationsLT[] calldata batch
)
external
override
returns (uint256[] memory streamIds)
{
// Check that the batch size is not zero.
uint256 batchSize = batch.length;
if (batchSize == 0) {
revert Errors.SablierV2Batch_BatchSizeZero();
}

// Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create
// transactions will revert if there is overflow.
uint256 i;
uint256 transferAmount;
for (i = 0; i < batchSize; ++i) {
unchecked {
transferAmount += batch[i].totalAmount;
}
}

// Perform the ERC-20 transfer and approve {SablierV2LockupTranched} to spend the amount of assets.
_handleTransfer(address(lockupTranched), asset, transferAmount);

// Create a stream for each element in the parameter array.
streamIds = new uint256[](batchSize);
for (i = 0; i < batchSize; ++i) {
// Create the stream.
streamIds[i] = lockupTranched.createWithDurations(
LockupTranched.CreateWithDurations({
sender: batch[i].sender,
recipient: batch[i].recipient,
totalAmount: batch[i].totalAmount,
asset: asset,
cancelable: batch[i].cancelable,
transferable: batch[i].transferable,
tranches: batch[i].tranches,
broker: batch[i].broker
})
);
}
}

/// @inheritdoc ISablierV2Batch
function createWithTimestampsLT(
ISablierV2LockupTranched lockupTranched,
IERC20 asset,
Batch.CreateWithTimestampsLT[] calldata batch
)
external
override
returns (uint256[] memory streamIds)
{
// Check that the batch size is not zero.
uint256 batchSize = batch.length;
if (batchSize == 0) {
revert Errors.SablierV2Batch_BatchSizeZero();
}

// Calculate the sum of all of stream amounts. It is safe to use unchecked addition because one of the create
// transactions will revert if there is overflow.
uint256 i;
uint256 transferAmount;
for (i = 0; i < batchSize; ++i) {
unchecked {
transferAmount += batch[i].totalAmount;
}
}

// Perform the ERC-20 transfer and approve {SablierV2LockupTranched} to spend the amount of assets.
_handleTransfer(address(lockupTranched), asset, transferAmount);

// Create a stream for each element in the parameter array.
streamIds = new uint256[](batchSize);
for (i = 0; i < batchSize; ++i) {
// Create the stream.
streamIds[i] = lockupTranched.createWithTimestamps(
LockupTranched.CreateWithTimestamps({
sender: batch[i].sender,
recipient: batch[i].recipient,
totalAmount: batch[i].totalAmount,
asset: asset,
cancelable: batch[i].cancelable,
transferable: batch[i].transferable,
startTime: batch[i].startTime,
tranches: batch[i].tranches,
broker: batch[i].broker
})
);
}
}

/*//////////////////////////////////////////////////////////////////////////
HELPER FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down
43 changes: 43 additions & 0 deletions src/interfaces/ISablierV2Batch.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.22;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";

import { Batch } from "../types/DataTypes.sol";

Expand Down Expand Up @@ -93,4 +94,46 @@ interface ISablierV2Batch {
)
external
returns (uint256[] memory streamIds);

/*//////////////////////////////////////////////////////////////////////////
SABLIER-V2-LOCKUP-TRANCHED
//////////////////////////////////////////////////////////////////////////*/

/// @notice Creates a batch of Lockup Tranched streams using `createWithDurations`.
///
/// @dev Requirements:
/// - There must be at least one element in `batch`.
/// - All requirements from {ISablierV2LockupTranched.createWithDurations} must be met for each stream.
///
/// @param lockupTranched The address of the {SablierV2LockupTranched} contract.
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param batch An array of structs, each encapsulating a subset of the parameters of
/// {SablierV2LockupTranched.createWithDurations}.
/// @return streamIds The ids of the newly created streams.
function createWithDurationsLT(
ISablierV2LockupTranched lockupTranched,
IERC20 asset,
Batch.CreateWithDurationsLT[] calldata batch
)
external
returns (uint256[] memory streamIds);

/// @notice Creates a batch of Lockup Tranched streams using `createWithTimestamps`.
///
/// @dev Requirements:
/// - There must be at least one element in `batch`.
/// - All requirements from {ISablierV2LockupTranched.createWithTimestamps} must be met for each stream.
///
/// @param lockupTranched The address of the {SablierV2LockupTranched} contract.
/// @param asset The contract address of the ERC-20 asset used for streaming.
/// @param batch An array of structs, each encapsulating a subset of the parameters of
/// {SablierV2LockupTranched.createWithTimestamps}.
/// @return streamIds The ids of the newly created streams.
function createWithTimestampsLT(
ISablierV2LockupTranched lockupTranched,
IERC20 asset,
Batch.CreateWithTimestampsLT[] calldata batch
)
external
returns (uint256[] memory streamIds);
}
27 changes: 26 additions & 1 deletion src/types/DataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity >=0.8.22;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { UD2x18 } from "@prb/math/src/UD2x18.sol";
import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol";
import { Broker, LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";
import { Broker, LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol";

library Batch {
/// @notice A struct encapsulating the lockup contract's address and the stream ids to cancel.
Expand Down Expand Up @@ -37,6 +37,18 @@ library Batch {
Broker broker;
}

/// @notice A struct encapsulating all parameters of {SablierV2LockupTranched.createWithDurations} except for the
/// asset.
struct CreateWithDurationsLT {
address sender;
address recipient;
uint128 totalAmount;
bool cancelable;
bool transferable;
LockupTranched.TrancheWithDuration[] tranches;
Broker broker;
}

/// @notice A struct encapsulating all parameters of {SablierV2LockupDynamic.createWithTimestamps} except for the
/// asset.
struct CreateWithTimestampsLD {
Expand All @@ -61,6 +73,19 @@ library Batch {
LockupLinear.Range range;
Broker broker;
}

/// @notice A struct encapsulating all parameters of {SablierV2LockupTranched.createWithTimestamps} except for the
/// asset.
struct CreateWithTimestampsLT {
address sender;
address recipient;
uint128 totalAmount;
bool cancelable;
bool transferable;
uint40 startTime;
LockupTranched.Tranche[] tranches;
Broker broker;
}
}

library MerkleLockup {
Expand Down
67 changes: 31 additions & 36 deletions test/Base.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ pragma solidity >=0.8.22 <0.9.0;

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { ISablierV2Comptroller } from "@sablier/v2-core/src/interfaces/ISablierV2Comptroller.sol";
import { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol";
import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol";
import { LockupDynamic, LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";
import { LockupDynamic, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol";

import { Assertions as V2CoreAssertions } from "@sablier/v2-core/test/utils/Assertions.sol";
import { Utils as V2CoreUtils } from "@sablier/v2-core/test/utils/Utils.sol";
Expand Down Expand Up @@ -44,7 +43,6 @@ abstract contract Base_Test is Assertions, DeployOptimized, Events, Merkle, V2Co

IERC20 internal dai;
ISablierV2Batch internal batch;
ISablierV2Comptroller internal comptroller;
Defaults internal defaults;
ISablierV2LockupDynamic internal lockupDynamic;
ISablierV2LockupLinear internal lockupLinear;
Expand Down Expand Up @@ -108,7 +106,6 @@ abstract contract Base_Test is Assertions, DeployOptimized, Events, Merkle, V2Co
vm.label({ account: address(merkleLockupFactory), newLabel: "MerkleLockupFactory" });
vm.label({ account: address(merkleLockupLL), newLabel: "MerkleLockupLL" });
vm.label({ account: address(defaults), newLabel: "Defaults" });
vm.label({ account: address(comptroller), newLabel: "Comptroller" });
vm.label({ account: address(lockupDynamic), newLabel: "LockupDynamic" });
vm.label({ account: address(lockupLinear), newLabel: "LockupLinear" });
vm.label({ account: address(lockupTranched), newLabel: "LockupTranched" });
Expand All @@ -118,38 +115,6 @@ abstract contract Base_Test is Assertions, DeployOptimized, Events, Merkle, V2Co
CALL EXPECTS
//////////////////////////////////////////////////////////////////////////*/

/// @dev Expects a call to {ISablierV2LockupDynamic.createWithDurations}.
function expectCallToCreateWithDurationsLD(LockupDynamic.CreateWithDurations memory params) internal {
vm.expectCall({
callee: address(lockupDynamic),
data: abi.encodeCall(ISablierV2LockupDynamic.createWithDurations, (params))
});
}

/// @dev Expects a call to {ISablierV2LockupLinear.createWithDurations}.
function expectCallToCreateWithDurationsLL(LockupLinear.CreateWithDurations memory params) internal {
vm.expectCall({
callee: address(lockupLinear),
data: abi.encodeCall(ISablierV2LockupLinear.createWithDurations, (params))
});
}

/// @dev Expects a call to {ISablierV2LockupDynamic.createWithTimestamps}.
function expectCallToCreateWithTimestampsLD(LockupDynamic.CreateWithTimestamps memory params) internal {
vm.expectCall({
callee: address(lockupDynamic),
data: abi.encodeCall(ISablierV2LockupDynamic.createWithTimestamps, (params))
});
}

/// @dev Expects a call to {ISablierV2LockupLinear.createWithTimestamps}.
function expectCallToCreateWithTimestampsLL(LockupLinear.CreateWithTimestamps memory params) internal {
vm.expectCall({
callee: address(lockupLinear),
data: abi.encodeCall(ISablierV2LockupLinear.createWithTimestamps, (params))
});
}

/// @dev Expects a call to {IERC20.transfer}.
function expectCallToTransfer(address to, uint256 amount) internal {
expectCallToTransfer(address(dai), to, amount);
Expand Down Expand Up @@ -200,6 +165,21 @@ abstract contract Base_Test is Assertions, DeployOptimized, Events, Merkle, V2Co
});
}

/// @dev Expects multiple calls to {ISablierV2LockupTranched.createWithDurations}, each with the specified
/// `params`.
function expectMultipleCallsToCreateWithDurationsLT(
uint64 count,
LockupTranched.CreateWithDurations memory params
)
internal
{
vm.expectCall({
callee: address(lockupTranched),
count: count,
data: abi.encodeCall(ISablierV2LockupTranched.createWithDurations, (params))
});
}

/// @dev Expects multiple calls to {ISablierV2LockupDynamic.createWithTimestamps}, each with the specified
/// `params`.
function expectMultipleCallsToCreateWithTimestampsLD(
Expand Down Expand Up @@ -230,6 +210,21 @@ abstract contract Base_Test is Assertions, DeployOptimized, Events, Merkle, V2Co
});
}

/// @dev Expects multiple calls to {ISablierV2LockupTranched.createWithTimestamps}, each with the specified
/// `params`.
function expectMultipleCallsToCreateWithTimestampsLT(
uint64 count,
LockupTranched.CreateWithTimestamps memory params
)
internal
{
vm.expectCall({
callee: address(lockupTranched),
count: count,
data: abi.encodeCall(ISablierV2LockupTranched.createWithTimestamps, (params))
});
}

/// @dev Expects multiple calls to {IERC20.transfer}.
function expectMultipleCallsToTransfer(uint64 count, address to, uint256 amount) internal {
vm.expectCall({ callee: address(dai), count: count, data: abi.encodeCall(IERC20.transfer, (to, amount)) });
Expand Down
2 changes: 1 addition & 1 deletion test/fork/Fork.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ abstract contract Fork_Test is Base_Test, V2CoreFuzzers {
/// @dev Deploys the v2 core dependencies.
// TODO: Remove this function once the v2 core contracts are deployed on Mainnet.
function deployDependencies() private {
(, lockupDynamic, lockupLinear, lockupTranched,) = new V2CorePrecompiles().deployCore(users.admin);
(lockupDynamic, lockupLinear, lockupTranched,) = new V2CorePrecompiles().deployCore(users.admin);
}
}
5 changes: 5 additions & 0 deletions test/fork/assets/USDC.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { CreateWithTimestamps_LockupDynamic_Batch_Fork_Test } from "../batch/createWithTimestampsLD.t.sol";
import { CreateWithTimestamps_LockupLinear_Batch_Fork_Test } from "../batch/createWithTimestampsLL.t.sol";
import { CreateWithTimestamps_LockupTranched_Batch_Fork_Test } from "../batch/createWithTimestampsLT.t.sol";
import { MerkleLockupLL_Fork_Test } from "../merkle-lockup/MerkleLockupLL.t.sol";
import { MerkleLockupLT_Fork_Test } from "../merkle-lockup/MerkleLockupLT.t.sol";

Expand All @@ -19,6 +20,10 @@ contract USDC_CreateWithTimestamps_LockupLinear_Batch_Fork_Test is
CreateWithTimestamps_LockupLinear_Batch_Fork_Test(usdc)
{ }

contract USDC_CreateWithTimestamps_LockupTranched_Batch_Fork_Test is
CreateWithTimestamps_LockupTranched_Batch_Fork_Test(usdc)
{ }

contract USDC_MerkleLockupLL_Fork_Test is MerkleLockupLL_Fork_Test(usdc) { }

contract USDC_MerkleLockupLT_Fork_Test is MerkleLockupLT_Fork_Test(usdc) { }
Loading

0 comments on commit 65a51db

Please sign in to comment.