Create a Lockup Dynamic Stream
Dynamic streams are streams with a custom streaming function. In this guide, we will show you how to create a Lockup Dynamic stream using Solidity.
This guide assumes that you have already gone through the Protocol Concepts section.
The code in this guide is not production-ready, and is implemented in a simplistic manner for the purpose of learning.
Set up a contract
Declare the Solidity version used to compile the contract:
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.22;
Import the relevant symbols from @sablier/lockup
:
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 { ISablierLockup } from "@sablier/lockup/src/interfaces/ISablierLockup.sol";
import { Broker, Lockup, LockupDynamic } from "@sablier/lockup/src/types/DataTypes.sol";
Create a contract called LockupDynamicStreamCreator
, and declare a constant DAI
of type IERC20
and a constant
LOCKUP
of type ISablierLockup
:
contract LockupDynamicStreamCreator {
IERC20 public constant DAI = IERC20(0x68194a729C2450ad26072b3D33ADaCbcef39D574);
ISablierLockup public constant LOCKUP = ISablierLockup(0xC2Da366fD67423b500cDF4712BdB41d0995b0794);
}
Also, these addresses are deployed on Sepolia. If you need to work with a different chain, Lockup addresses can be obtained from the Lockup Deployments page.
There are two create functions in the Lockup contract that can be used to create Dynamic streams:
createWithDurationsLD
: takes duration and calculates the segment timestamps based on the provided durations.createWithTimestampsLD
: takes UNIX timestamps for segment.
Which one you choose depends upon your use case. In this guide, we will use createWithTimestampsLD
.
Function definition
Define a function called createStream
which takes two parameters, amount0
and amount1
, and which returns the id of
the created stream:
function createStream(uint128 amount0, uint128 amount1) public returns (uint256 streamId) {
// ...
}
Next, sum up the amount0
and amount1
parameters to get the total amount of the stream, which will be needed in many
of the steps below:
uint256 totalAmount = amount0 + amount1;
ERC-20 steps
To create a stream, the caller must approve the creator contract to pull the tokens from the calling address's account. Then, we have to approve the Lockup contract to pull the tokens that the creator contract will be in possession of after they are transferred from the calling address (you):
// Transfer the provided amount of DAI tokens to this contract
DAI.transferFrom(msg.sender, address(this), totalAmount);
// Approve the Sablier contract to spend DAI
DAI.approve(address(LOCKUP), totalAmount);
For more guidance on how to approve and transfer ERC-20 tokens, see this article on the Ethereum website.
Parameters
The struct associated with createWithTimestampsLD
are
Lockup.CreateWithTimestamps
(a shared struct
across all the lockup streams) and
LockupDynamic.Segment
.
LockupDynamic.CreateWithTimestamps memory params;
LockupDynamic.Segment[] memory segments = new LockupDynamic.Segment[](2);
Let's review each parameter in detail.
Let's review each parameter in detail.
Broker
An optional parameter that can be set in order to charge a fee as a percentage of totalAmount
.
In the following example, we will leave this parameter uninitialized (i.e. set to zero), because it doesn't make sense to charge yourself a fee. In practice, this parameter will mostly be used by front-end applications.
params.broker = Broker(address(0), ud60x18(0));
Wondering what's up with that ud60x18
function? It's a casting function that wraps a basic integer to the UD60x18
value type. This type is part of the math library PRBMath, which is used in
Sablier for fixed-point calculations.
Cancelable
Boolean that indicates whether the stream will be cancelable or not.
params.cancelable = true;
Recipient
The address receiving the tokens:
params.recipient = address(0xCAFE);
Sender
The address streaming the tokens, with the ability to cancel the stream:
params.sender = msg.sender;
Token
The contract address of the ERC-20 token used for streaming. In this example, we will stream DAI:
params.token = DAI;
Total amount
The total amount of ERC-20 tokens to be paid, including the stream deposit and any potential fees, all denoted in units of the asset's decimals.
params.totalAmount = totalAmount;
Transferable
Boolean that indicates whether the stream will be transferable or not.
params.transferable = true;
Start Time and End Time
The start and end timestamps for the stream. Note that the end timestamps much match the timestamp of the last segment.
params.timestamps.start = uint40(block.timestamp + 100 seconds);
params.timestamps.end = uint40(block.timestamp + 52 weeks);
Segments
Segments are what the protocol uses to compose the custom distribution curve of a Dynamic stream. For a full exposition of segments, see the Segments guide.
The term "segment" refers to the splitting of the stream into separate partitions, with each segment characterized by a
specific amount, exponent, and timestamp. These segments are supplied to the function in the form of an array containing
LockupDynamic.Segment
structs.
Let's define two dummy segments:
segments[0] = LockupDynamic.Segment({
amount: amount0,
exponent: ud2x18(1e18),
timestamp: uint40(block.timestamp + 4 weeks)
});
segments[1] = (
LockupDynamic.Segment({
amount: amount1,
exponent: ud2x18(3.14e18),
timestamp: uint40(block.timestamp + 52 weeks)
})
);
In this example, the first segment (amount0
) will stream much faster than the second segment (amount1
), because the
exponents are different. As a rule of thumb: the higher the exponent, the slower the stream.
The segment timestamp must be in ascending order.
The ud2x18
function wraps a basic integer to the UD2x18
value type, which is part of the
PRBMath library.
Invoke the create function
With all parameters set, we can now call the createWithTimestampsLD
function, and assign the id of the newly created
stream to a variable:
streamId = LOCKUP.createWithTimestampsLD(params, segments);
Full code
Below you can see the full code. You can also access the code on GitHub through this link.
loading...