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/add tranched batches #300

Merged
merged 4 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading