Skip to content

Commit

Permalink
feat: add unlock amounts functionality
Browse files Browse the repository at this point in the history
refactor: some polishes

docs: update explanatory comments
test: add new test branches in create function

test: streamed amount in lockup linear

test: fix common streamed amount tests
test: remove unneeded branches

build: update deps

test: fix tranches default function

feat: include unlock amounts in StreamLL

docs: fix some comments
test: improve fuzz streamed amount tests
test: update fork with unlock amounts

test: fix last failing tests

docs: last polishes
  • Loading branch information
andreivladbrg committed Nov 13, 2024
1 parent 4d54d7b commit 8e1364a
Show file tree
Hide file tree
Showing 64 changed files with 696 additions and 318 deletions.
2 changes: 1 addition & 1 deletion .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "solhint:recommended",
"rules": {
"avoid-low-level-calls": "off",
"code-complexity": ["error", 9],
"code-complexity": ["error", 10],
"compiler-version": ["error", ">=0.8.22"],
"contract-name-camelcase": "off",
"const-name-snakecase": "off",
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@openzeppelin/contracts": "5.0.2",
"@prb/math": "4.0.3"
"@prb/math": "4.1.0"
},
"devDependencies": {
"forge-std": "github:foundry-rs/forge-std#v1.8.2",
Expand Down
2 changes: 1 addition & 1 deletion script/core/Init.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity >=0.8.22 <0.9.0;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ud2x18 } from "@prb/math/src/UD2x18.sol";
import { ud60x18 } from "@prb/math/src/UD60x18.sol";

import { Solarray } from "solarray/src/Solarray.sol";

import { ISablierLockup } from "../../src/core/interfaces/ISablierLockup.sol";
Expand Down Expand Up @@ -54,6 +53,7 @@ contract Init is BaseScript {
transferable: true,
broker: Broker(address(0), ud60x18(0))
}),
LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }),
LockupLinear.Durations({ cliff: cliffDurations[i], total: totalDurations[i] })
);
}
Expand Down
2 changes: 2 additions & 0 deletions script/periphery/CreateMerkleLL.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity >=0.8.22 <0.9.0;
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

import { ISablierLockup } from "../../src/core/interfaces/ISablierLockup.sol";
import { LockupLinear } from "../../src/core/types/DataTypes.sol";
import { ISablierMerkleFactory } from "../../src/periphery/interfaces/ISablierMerkleFactory.sol";
import { ISablierMerkleLL } from "../../src/periphery/interfaces/ISablierMerkleLL.sol";
import { MerkleBase, MerkleLL } from "../../src/periphery/types/DataTypes.sol";
Expand Down Expand Up @@ -37,6 +38,7 @@ contract CreateMerkleLL is BaseScript {
cliffDuration: 30 days,
totalDuration: 90 days
}),
unlockAmounts: LockupLinear.UnlockAmounts({ start: 0, cliff: 0 }),
aggregateAmount: 10_000e18,
recipientCount: 100
});
Expand Down
117 changes: 75 additions & 42 deletions src/core/SablierLockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
/// @dev Stream tranches mapped by stream IDs. This is used in Lockup Tranched models.
mapping(uint256 streamId => LockupTranched.Tranche[] tranches) internal _tranches;

/// @dev Unlock amounts mapped by stream IDs. This is used in Lockup Linear models.
mapping(uint256 streamId => LockupLinear.UnlockAmounts unlockAmounts) internal _unlockAmounts;

/*//////////////////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -108,6 +111,17 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
tranches = _tranches[streamId];
}

/// @inheritdoc ISablierLockup
function getUnlockAmounts(uint256 streamId)
external
view
override
notNull(streamId)
returns (LockupLinear.UnlockAmounts memory unlockAmounts)
{
unlockAmounts = _unlockAmounts[streamId];
}

/*//////////////////////////////////////////////////////////////////////////
USER-FACING NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -148,6 +162,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
/// @inheritdoc ISablierLockup
function createWithDurationsLL(
Lockup.CreateWithDurations calldata params,
LockupLinear.UnlockAmounts calldata unlockAmounts,
LockupLinear.Durations calldata durations
)
external
Expand All @@ -172,17 +187,20 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {

// Checks, Effects and Interactions: create the stream.
streamId = _createLL(
Lockup.CreateWithTimestamps({
sender: params.sender,
recipient: params.recipient,
totalAmount: params.totalAmount,
asset: params.asset,
cancelable: params.cancelable,
transferable: params.transferable,
timestamps: timestamps,
broker: params.broker
}),
cliffTime
CreateLLParams(
Lockup.CreateWithTimestamps({
sender: params.sender,
recipient: params.recipient,
totalAmount: params.totalAmount,
asset: params.asset,
cancelable: params.cancelable,
transferable: params.transferable,
timestamps: timestamps,
broker: params.broker
}),
unlockAmounts,
cliffTime
)
);
}

Expand Down Expand Up @@ -236,6 +254,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
/// @inheritdoc ISablierLockup
function createWithTimestampsLL(
Lockup.CreateWithTimestamps calldata params,
LockupLinear.UnlockAmounts calldata unlockAmounts,
uint40 cliffTime
)
external
Expand All @@ -244,7 +263,7 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
returns (uint256 streamId)
{
// Checks, Effects and Interactions: create the stream.
streamId = _createLL(params, cliffTime);
streamId = _createLL(CreateLLParams(params, unlockAmounts, cliffTime));
}

/// @inheritdoc ISablierLockup
Expand All @@ -267,17 +286,18 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {

/// @inheritdoc SablierLockupBase
function _calculateStreamedAmount(uint256 streamId) internal view override returns (uint128) {
Lockup.Timestamps memory timestamps =
Lockup.Timestamps({ start: _streams[streamId].startTime, end: _streams[streamId].endTime });

// If the start time is in the future, return zero.
uint40 blockTimestamp = uint40(block.timestamp);
uint40 startTime = _streams[streamId].startTime;
if (startTime >= blockTimestamp) {
if (timestamps.start >= blockTimestamp) {
return 0;
}

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

Expand All @@ -288,17 +308,18 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
if (lockupModel == Lockup.Model.LOCKUP_DYNAMIC) {
streamedAmount = VestingMath.calculateLockupDynamicStreamedAmount({
segments: _segments[streamId],
startTime: startTime,
startTime: timestamps.start,
withdrawnAmount: _streams[streamId].amounts.withdrawn
});
}
// Calculate streamed amount for Lockup Linear model.
else if (lockupModel == Lockup.Model.LOCKUP_LINEAR) {
streamedAmount = VestingMath.calculateLockupLinearStreamedAmount({
depositedAmount: depositedAmount,
startTime: startTime,
startTime: timestamps.start,
cliffTime: _cliffs[streamId],
endTime: endTime,
endTime: timestamps.end,
unlockAmounts: _unlockAmounts[streamId],
withdrawnAmount: _streams[streamId].amounts.withdrawn
});
}
Expand Down Expand Up @@ -408,36 +429,47 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
});
}

struct CreateLLParams {
Lockup.CreateWithTimestamps createWithTimestamps;
LockupLinear.UnlockAmounts unlockAmounts;
uint40 cliffTime;
}

/// @dev See the documentation for the user-facing functions that call this internal function.
function _createLL(
Lockup.CreateWithTimestamps memory params,
uint40 cliffTime
)
internal
returns (uint256 streamId)
{
function _createLL(CreateLLParams memory params) internal returns (uint256 streamId) {
// Check: validate the user-provided parameters and cliff time.
Lockup.CreateAmounts memory createAmounts = Helpers.checkCreateLockupLinear({
sender: params.sender,
timestamps: params.timestamps,
cliffTime: cliffTime,
totalAmount: params.totalAmount,
brokerFee: params.broker.fee,
sender: params.createWithTimestamps.sender,
timestamps: params.createWithTimestamps.timestamps,
cliffTime: params.cliffTime,
totalAmount: params.createWithTimestamps.totalAmount,
unlockAmounts: params.unlockAmounts,
brokerFee: params.createWithTimestamps.broker.fee,
maxBrokerFee: MAX_BROKER_FEE
});

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

// Effect: set the start unlock amount if its non-zero.
if (params.unlockAmounts.start > 0) {
_unlockAmounts[streamId].start = params.unlockAmounts.start;
}

// Effect: update cliff time if its non-zero.
if (cliffTime > 0) {
_cliffs[streamId] = cliffTime;
if (params.cliffTime > 0) {
_cliffs[streamId] = params.cliffTime;

// Effect: set the cliff unlock amount if its non-zero.
if (params.unlockAmounts.cliff > 0) {
_unlockAmounts[streamId].cliff = params.unlockAmounts.cliff;
}
}

// Effect: create the stream, mint the NFT and transfer the deposit amount.
_create({
streamId: streamId,
params: params,
params: params.createWithTimestamps,
createAmounts: createAmounts,
lockupModel: Lockup.Model.LOCKUP_LINEAR
});
Expand All @@ -446,15 +478,16 @@ contract SablierLockup is ISablierLockup, SablierLockupBase {
emit ISablierLockup.CreateLockupLinearStream({
streamId: streamId,
funder: msg.sender,
sender: params.sender,
recipient: params.recipient,
sender: params.createWithTimestamps.sender,
recipient: params.createWithTimestamps.recipient,
amounts: createAmounts,
asset: params.asset,
cancelable: params.cancelable,
transferable: params.transferable,
timestamps: params.timestamps,
cliffTime: cliffTime,
broker: params.broker.account
asset: params.createWithTimestamps.asset,
cancelable: params.createWithTimestamps.cancelable,
transferable: params.createWithTimestamps.transferable,
timestamps: params.createWithTimestamps.timestamps,
cliffTime: params.cliffTime,
unlockAmounts: params.unlockAmounts,
broker: params.createWithTimestamps.broker.account
});
}

Expand Down
23 changes: 23 additions & 0 deletions src/core/interfaces/ISablierLockup.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ interface ISablierLockup is ISablierLockupBase {
/// @param transferable Boolean indicating whether the stream NFT is transferable or not.
/// @param timestamps Struct encapsulating (i) the stream's start time and (ii) end time, all as Unix timestamps.
/// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff.
/// @param unlockAmounts Struct encapsulating (i) the amount unlocked at the start time and (ii) the amount unlocked
/// at the cliff time.
/// @param broker The address of the broker who has helped create the stream, e.g. a front-end website.
event CreateLockupLinearStream(
uint256 streamId,
Expand All @@ -64,6 +66,7 @@ interface ISablierLockup is ISablierLockupBase {
bool transferable,
Lockup.Timestamps timestamps,
uint40 cliffTime,
LockupLinear.UnlockAmounts unlockAmounts,
address broker
);

Expand Down Expand Up @@ -110,13 +113,24 @@ interface ISablierLockup is ISablierLockupBase {
/// @notice Retrieves the segments used to compose the dynamic distribution function.
/// @dev Reverts if `streamId` references a null stream or a non Lockup Dynamic stream.
/// @param streamId The stream ID for the query.
/// @return segments See the documentation in {DataTypes}.
function getSegments(uint256 streamId) external view returns (LockupDynamic.Segment[] memory segments);

/// @notice Retrieves the tranches used to compose the tranched distribution function.
/// @dev Reverts if `streamId` references a null stream or a non Lockup Tranched stream.
/// @param streamId The stream ID for the query.
/// @return tranches See the documentation in {DataTypes}.
function getTranches(uint256 streamId) external view returns (LockupTranched.Tranche[] memory tranches);

/// @notice Retrieves the unlock amounts used to compose the linear distribution function.
/// @dev Reverts if `streamId` references a null stream.
/// @param streamId The stream ID for the query.
/// @return unlockAmounts See the documentation in {DataTypes}.
function getUnlockAmounts(uint256 streamId)
external
view
returns (LockupLinear.UnlockAmounts memory unlockAmounts);

/*//////////////////////////////////////////////////////////////////////////
NON-CONSTANT FUNCTIONS
//////////////////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -152,9 +166,12 @@ interface ISablierLockup is ISablierLockupBase {
///
/// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}.
/// @param durations Struct encapsulating (i) cliff period duration and (ii) total stream duration, both in seconds.
/// @param unlockAmounts Struct encapsulating (i) the amount unlocked at the start time and (ii) the amount unlocked
/// at the cliff time.
/// @return streamId The ID of the newly created stream.
function createWithDurationsLL(
Lockup.CreateWithDurations calldata params,
LockupLinear.UnlockAmounts calldata unlockAmounts,
LockupLinear.Durations calldata durations
)
external
Expand Down Expand Up @@ -230,13 +247,19 @@ interface ISablierLockup is ISablierLockupBase {
/// `params.timestamps.end`.
/// - `params.recipient` must not be the zero address.
/// - `params.sender` must not be the zero address.
/// - The sum of `params.unlockAmounts.start` and `params.unlockAmounts.cliff` must be less than or equal to
/// deposit amount.
/// - If `params.timestamps.cliff` not set, the `params.unlockAmounts.cliff` must be zero.
/// - `msg.sender` must have allowed this contract to spend at least `params.totalAmount` assets.
///
/// @param params Struct encapsulating the function parameters, which are documented in {DataTypes}.
/// @param cliffTime The Unix timestamp for the cliff period's end. A value of zero means there is no cliff.
/// @param unlockAmounts Struct encapsulating (i) the amount unlocked at the start time and (ii) the amount unlocked
/// at the cliff time.
/// @return streamId The ID of the newly created stream.
function createWithTimestampsLL(
Lockup.CreateWithTimestamps calldata params,
LockupLinear.UnlockAmounts calldata unlockAmounts,
uint40 cliffTime
)
external
Expand Down
8 changes: 8 additions & 0 deletions src/core/libraries/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ library Errors {
/// @notice Thrown when trying to create a linear stream with a cliff time not strictly less than the end time.
error SablierHelpers_CliffTimeNotLessThanEndTime(uint40 cliffTime, uint40 endTime);

/// @notice Thrown when trying to create a stream with a non zero cliff unlock amount when the cliff time is zero.
error SablierHelpers_CliffTimeZeroUnlockAmountNotZero(uint128 cliffUnlockAmount);

/// @notice Thrown when trying to create a dynamic stream with a deposit amount not equal to the sum of the segment
/// amounts.
error SablierHelpers_DepositAmountNotEqualToSegmentAmountsSum(uint128 depositAmount, uint128 segmentAmountsSum);
Expand Down Expand Up @@ -95,6 +98,11 @@ library Errors {
/// @notice Thrown when trying to create a tranched stream with unordered tranche timestamps.
error SablierHelpers_TrancheTimestampsNotOrdered(uint256 index, uint40 previousTimestamp, uint40 currentTimestamp);

/// @notice Thrown when trying to create a stream with the sum of the unlock amounts greater than deposit amount.
error SablierHelpers_UnlockAmountsSumTooHigh(
uint128 depositAmount, uint128 startUnlockAmount, uint128 cliffUnlockAmount
);

/*//////////////////////////////////////////////////////////////////////////
SABLIER-LOCKUP-BASE
//////////////////////////////////////////////////////////////////////////*/
Expand Down
Loading

0 comments on commit 8e1364a

Please sign in to comment.