Skip to content

Commit

Permalink
feat: implement SablierV2LockupTranched contract
Browse files Browse the repository at this point in the history
build: add SablierV2LockupTranched in shell scripts
test: test SablierV2LockupTranched contract
test: deploy core contracts in Base_Test
test: use changePrank instead of vm.StartPrank
  • Loading branch information
andreivladbrg committed Feb 15, 2024
1 parent c47eebd commit 41ad50a
Show file tree
Hide file tree
Showing 62 changed files with 4,749 additions and 80 deletions.
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"SablierV2Comptroller",
"SablierV2LockupDynamic",
"SablierV2LockupLinear",
"SablierV2LockupTranched",
"SablierV2NFTDescriptor",
]
optimizer = true
Expand Down Expand Up @@ -68,6 +69,7 @@
[profile.smt.model_checker.contracts]
"src/SablierV2LockupDynamic.sol" = ["SablierV2LockupDynamic"]
"src/SablierV2LockupLinear.sol" = ["SablierV2LockupLinear"]
"src/SablierV2LockupTranched.sol" = ["SablierV2LockupTranched"]
"src/SablierV2NFTDescriptor.sol" = ["SablierV2NFTDescriptor"]

# Test the optimized contracts without re-compiling them
Expand Down
2 changes: 2 additions & 0 deletions shell/prepare-artifacts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ FOUNDRY_PROFILE=optimized forge build
cp out-optimized/SablierV2Comptroller.sol/SablierV2Comptroller.json $artifacts
cp out-optimized/SablierV2LockupDynamic.sol/SablierV2LockupDynamic.json $artifacts
cp out-optimized/SablierV2LockupLinear.sol/SablierV2LockupLinear.json $artifacts
cp out-optimized/SablierV2LockupTranched.sol/SablierV2LockupTranched.json $artifacts
cp out-optimized/SablierV2NFTDescriptor.sol/SablierV2NFTDescriptor.json $artifacts

interfaces=./artifacts/interfaces
Expand All @@ -34,6 +35,7 @@ cp out-optimized/ISablierV2Comptroller.sol/ISablierV2Comptroller.json $interface
cp out-optimized/ISablierV2Lockup.sol/ISablierV2Lockup.json $interfaces
cp out-optimized/ISablierV2LockupDynamic.sol/ISablierV2LockupDynamic.json $interfaces
cp out-optimized/ISablierV2LockupLinear.sol/ISablierV2LockupLinear.json $interfaces
cp out-optimized/ISablierV2LockupTranched.sol/ISablierV2LockupTranched.json $interfaces
cp out-optimized/ISablierV2NFTDescriptor.sol/ISablierV2NFTDescriptor.json $interfaces

erc20=./artifacts/interfaces/erc20
Expand Down
2 changes: 2 additions & 0 deletions shell/update-precompiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ FOUNDRY_PROFILE=optimized forge build
comptroller=$(cat out-optimized/SablierV2Comptroller.sol/SablierV2Comptroller.json | jq -r '.bytecode.object' | cut -c 3-)
lockup_dynamic=$(cat out-optimized/SablierV2LockupDynamic.sol/SablierV2LockupDynamic.json | jq -r '.bytecode.object' | cut -c 3-)
lockup_linear=$(cat out-optimized/SablierV2LockupLinear.sol/SablierV2LockupLinear.json | jq -r '.bytecode.object' | cut -c 3-)
lockup_tranched=$(cat out-optimized/SablierV2LockupTranched.sol/SablierV2LockupTranched.json | jq -r '.bytecode.object' | cut -c 3-)
nft_descriptor=$(cat out-optimized/SablierV2NFTDescriptor.sol/SablierV2NFTDescriptor.json | jq -r '.bytecode.object' | cut -c 3-)

precompiles_path="test/utils/Precompiles.sol"
Expand All @@ -27,6 +28,7 @@ fi
sd "(BYTECODE_COMPTROLLER =)[^;]+;" "\$1 hex\"$comptroller\";" $precompiles_path
sd "(BYTECODE_LOCKUP_DYNAMIC =)[^;]+;" "\$1 hex\"$lockup_dynamic\";" $precompiles_path
sd "(BYTECODE_LOCKUP_LINEAR =)[^;]+;" "\$1 hex\"$lockup_linear\";" $precompiles_path
sd "(BYTECODE_LOCKUP_TRANCHED =)[^;]+;" "\$1 hex\"$lockup_tranched\";" $precompiles_path
sd "(BYTECODE_NFT_DESCRIPTOR =)[^;]+;" "\$1 hex\"$nft_descriptor\";" $precompiles_path

# Reformat the code with Forge
Expand Down
306 changes: 306 additions & 0 deletions src/SablierV2LockupTranched.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
// SPDX-License-Identifier: BUSL-1.1
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 { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { PRBMathCastingUint128 as CastingUint128 } from "@prb/math/src/casting/Uint128.sol";
import { PRBMathCastingUint40 as CastingUint40 } from "@prb/math/src/casting/Uint40.sol";
import { UD60x18 } from "@prb/math/src/UD60x18.sol";

import { SablierV2Lockup } from "./abstracts/SablierV2Lockup.sol";
import { ISablierV2Comptroller } from "./interfaces/ISablierV2Comptroller.sol";
import { ISablierV2LockupTranched } from "./interfaces/ISablierV2LockupTranched.sol";
import { ISablierV2NFTDescriptor } from "./interfaces/ISablierV2NFTDescriptor.sol";
import { Helpers } from "./libraries/Helpers.sol";
import { Lockup, LockupTranched } from "./types/DataTypes.sol";

/*
███████╗ █████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██████╗
██╔════╝██╔══██╗██╔══██╗██║ ██║██╔════╝██╔══██╗ ██║ ██║╚════██╗
███████╗███████║██████╔╝██║ ██║█████╗ ██████╔╝ ██║ ██║ █████╔╝
╚════██║██╔══██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ╚██╗ ██╔╝██╔═══╝
███████║██║ ██║██████╔╝███████╗██║███████╗██║ ██║ ╚████╔╝ ███████╗
╚══════╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝
██╗ ██████╗ ██████╗██╗ ██╗██╗ ██╗██████╗ ████████╗██████╗ █████╗ ███╗ ██╗ ██████╗██╗ ██╗███████╗██████╗
██║ ██╔═══██╗██╔════╝██║ ██╔╝██║ ██║██╔══██╗ ╚══██╔══╝██╔══██╗██╔══██╗████╗ ██║██╔════╝██║ ██║██╔════╝██╔══██╗
██║ ██║ ██║██║ █████╔╝ ██║ ██║██████╔╝ ██║ ██████╔╝███████║██╔██╗ ██║██║ ███████║█████╗ ██║ ██║
██║ ██║ ██║██║ ██╔═██╗ ██║ ██║██╔═══╝ ██║ ██╔══██╗██╔══██║██║╚██╗██║██║ ██╔══██║██╔══╝ ██║ ██║
███████╗╚██████╔╝╚██████╗██║ ██╗╚██████╔╝██║ ██║ ██║ ██║██║ ██║██║ ╚████║╚██████╗██║ ██║███████╗██████╔╝
╚══════╝ ╚═════╝ ╚═════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝
*/

/// @title SablierV2LockupTranched
/// @notice See the documentation in {ISablierV2LockupTranched}.
contract SablierV2LockupTranched is
ISablierV2LockupTranched, // 1 inherited component
SablierV2Lockup // 14 inherited components
{
using CastingUint128 for uint128;
using CastingUint40 for uint40;
using SafeERC20 for IERC20;

/*//////////////////////////////////////////////////////////////////////////
STATE VARIABLES
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2LockupTranched
uint256 public immutable override MAX_TRANCHE_COUNT;

/// @dev Stream tranches mapped by stream ids.
mapping(uint256 id => LockupTranched.Tranche[] tranches) internal _tranches;

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/

/// @dev Emits a {TransferAdmin} event.
/// @param initialAdmin The address of the initial contract admin.
/// @param initialComptroller The address of the initial comptroller.
/// @param initialNFTDescriptor The address of the NFT descriptor contract.
/// @param maxTrancheCount The maximum number of tranches allowed in a stream.
constructor(
address initialAdmin,
ISablierV2Comptroller initialComptroller,
ISablierV2NFTDescriptor initialNFTDescriptor,
uint256 maxTrancheCount
)
ERC721("Sablier V2 Lockup Tranche NFT", "SAB-V2-LOCKUP-DYN")
SablierV2Lockup(initialAdmin, initialComptroller, initialNFTDescriptor)
{
MAX_TRANCHE_COUNT = maxTrancheCount;
nextStreamId = 1;
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2LockupTranched
function getRange(uint256 streamId)
external
view
override
notNull(streamId)
returns (LockupTranched.Range memory range)
{
range = LockupTranched.Range({ start: _streams[streamId].startTime, end: _streams[streamId].endTime });
}

/// @inheritdoc ISablierV2LockupTranched
function getStream(uint256 streamId)
external
view
override
notNull(streamId)
returns (LockupTranched.StreamLT memory stream)
{
Lockup.Stream memory lockupStream = _streams[streamId];

// Settled streams cannot be canceled.
if (_statusOf(streamId) == Lockup.Status.SETTLED) {
lockupStream.isCancelable = false;
}

stream = LockupTranched.StreamLT({
amounts: lockupStream.amounts,
asset: lockupStream.asset,
endTime: lockupStream.endTime,
isCancelable: lockupStream.isCancelable,
isTransferable: lockupStream.isTransferable,
isDepleted: lockupStream.isDepleted,
isStream: lockupStream.isStream,
sender: lockupStream.sender,
startTime: lockupStream.startTime,
tranches: _tranches[streamId],
wasCanceled: lockupStream.wasCanceled
});
}

/// @inheritdoc ISablierV2LockupTranched
function getTranches(uint256 streamId)
external
view
override
notNull(streamId)
returns (LockupTranched.Tranche[] memory tranches)
{
tranches = _tranches[streamId];
}

/// @inheritdoc ISablierV2LockupTranched
function streamedAmountOf(uint256 streamId)
public
view
override(SablierV2Lockup, ISablierV2LockupTranched)
returns (uint128)
{
return super.streamedAmountOf(streamId);
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc ISablierV2LockupTranched
function createWithDurations(LockupTranched.CreateWithDurations calldata params)
external
override
noDelegateCall
returns (uint256 streamId)
{
// Checks: check the durations and generate the canonical tranches.
LockupTranched.Tranche[] memory tranches = Helpers.checkDurationsAndCalculateTimestamps(params.tranches);

// Checks, Effects and Interactions: create the stream.
streamId = _createWithTimestamps(
LockupTranched.CreateWithTimestamps({
sender: params.sender,
recipient: params.recipient,
totalAmount: params.totalAmount,
asset: params.asset,
cancelable: params.cancelable,
transferable: params.transferable,
startTime: uint40(block.timestamp),
tranches: tranches,
broker: params.broker
})
);
}

/// @inheritdoc ISablierV2LockupTranched
function createWithTimestamps(LockupTranched.CreateWithTimestamps calldata params)
external
override
noDelegateCall
returns (uint256 streamId)
{
// Checks, Effects and Interactions: create the stream.
streamId = _createWithTimestamps(params);
}

/*//////////////////////////////////////////////////////////////////////////
INTERNAL CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @inheritdoc SablierV2Lockup
function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) {
uint40 currentTime = uint40(block.timestamp);

LockupTranched.Tranche[] memory tranches = _tranches[streamId];

// If the first timestamp in the tranches is in the future, return zero.
if (tranches[0].timestamp > currentTime) {
return 0;
}

// If the end time is not in the future, return the deposited amount.
if (_streams[streamId].endTime <= currentTime) {
return _streams[streamId].amounts.deposited;
}

// Sum the amounts in all tranches that precede the current time.
uint128 streamedAmount = tranches[0].amount;
uint40 currentTrancheTimestamp = tranches[1].timestamp;
uint256 index = 1;

// Using unchecked arithmetic is safe here because the sums of the tranche amounts are equal to the total amount
// at this point.
unchecked {
while (currentTrancheTimestamp <= currentTime) {
streamedAmount += tranches[index].amount;
index += 1;
currentTrancheTimestamp = tranches[index].timestamp;
}
}

return streamedAmount;
}

/*//////////////////////////////////////////////////////////////////////////
INTERNAL NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @dev See the documentation for the user-facing functions that call this internal function.
function _createWithTimestamps(LockupTranched.CreateWithTimestamps memory params)
internal
returns (uint256 streamId)
{
// Safe Interactions: query the protocol fee. This is safe because it's a known Sablier contract that does
// not call other unknown contracts.
UD60x18 protocolFee = comptroller.protocolFees(params.asset);

// Checks: check the fees and calculate the fee amounts.
Lockup.CreateAmounts memory createAmounts =
Helpers.checkAndCalculateFees(params.totalAmount, protocolFee, params.broker.fee, MAX_FEE);

// Checks: validate the user-provided parameters.
Helpers.checkCreateWithTimestamps(createAmounts.deposit, params.tranches, MAX_TRANCHE_COUNT, params.startTime);

// Load the stream id in a variable.
streamId = nextStreamId;

// Effects: create the stream.
Lockup.Stream storage stream = _streams[streamId];
stream.amounts.deposited = createAmounts.deposit;
stream.asset = params.asset;
stream.isCancelable = params.cancelable;
stream.isTransferable = params.transferable;
stream.isStream = true;
stream.sender = params.sender;
stream.startTime = params.startTime;

unchecked {
// The tranche count cannot be zero at this point.
uint256 trancheCount = params.tranches.length;
stream.endTime = params.tranches[trancheCount - 1].timestamp;

// Effects: store the tranches. Since Solidity lacks a syntax for copying arrays directly from
// memory to storage, a manual approach is necessary. See https://github.com/ethereum/solidity/issues/12783.
for (uint256 i = 0; i < trancheCount; ++i) {
_tranches[streamId].push(params.tranches[i]);
}

// Effects: bump the next stream id and record the protocol fee.
// Using unchecked arithmetic because these calculations cannot realistically overflow, ever.
nextStreamId = streamId + 1;
protocolRevenues[params.asset] = protocolRevenues[params.asset] + createAmounts.protocolFee;
}

// Effects: mint the NFT to the recipient.
_mint({ to: params.recipient, tokenId: streamId });

// Interactions: transfer the deposit and the protocol fee.
// Using unchecked arithmetic because the deposit and the protocol fee are bounded by the total amount.
unchecked {
params.asset.safeTransferFrom({
from: msg.sender,
to: address(this),
value: createAmounts.deposit + createAmounts.protocolFee
});
}

// Interactions: pay the broker fee, if not zero.
if (createAmounts.brokerFee > 0) {
params.asset.safeTransferFrom({ from: msg.sender, to: params.broker.account, value: createAmounts.brokerFee });
}

// Log the newly created stream.
emit ISablierV2LockupTranched.CreateLockupTranchedStream({
streamId: streamId,
funder: msg.sender,
sender: params.sender,
recipient: params.recipient,
amounts: createAmounts,
asset: params.asset,
cancelable: params.cancelable,
transferable: params.transferable,
tranches: params.tranches,
range: LockupTranched.Range({ start: stream.startTime, end: stream.endTime }),
broker: params.broker.account
});
}
}
8 changes: 4 additions & 4 deletions src/interfaces/ISablierV2LockupDynamic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,6 @@ interface ISablierV2LockupDynamic is ISablierV2Lockup {
CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/

/// @notice The maximum number of segments allowed in a stream.
/// @dev This is initialized at construction time and cannot be changed later.
function MAX_SEGMENT_COUNT() external view returns (uint256);

/// @notice Retrieves the stream's range, which is a struct containing (i) the stream's start time and (ii) end
/// time, both as Unix timestamps.
/// @dev Reverts if `streamId` references a null stream.
Expand All @@ -64,6 +60,10 @@ interface ISablierV2LockupDynamic is ISablierV2Lockup {
/// @param streamId The stream id for the query.
function getStream(uint256 streamId) external view returns (LockupDynamic.StreamLD memory stream);

/// @notice The maximum number of segments allowed in a stream.
/// @dev This is initialized at construction time and cannot be changed later.
function MAX_SEGMENT_COUNT() external view returns (uint256);

/// @notice Calculates the amount streamed to the recipient, denoted in units of the asset's decimals.
///
/// When the stream is warm, the streaming function is:
Expand Down
Loading

0 comments on commit 41ad50a

Please sign in to comment.