Skip to main content

Create a Lockup Dynamic stream

Lockup Dynamic are streams with custom streaming curves. In this guide, we will show you how to create a Lockup Dynamic stream programmatically.

note

This guide assumes that you have already read the Protocol Concepts section.

caution

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.13;

Import the relevant symbols from @sablier/v2-core:

import { ISablierV2LockupDynamic } from "@sablier/v2-core/interfaces/ISablierV2LockupDynamic.sol";
import { Broker, LockupDynamic } from "@sablier/v2-core/types/DataTypes.sol";
import { ud2x18, ud60x18 } from "@sablier/v2-core/types/Math.sol";
import { IERC20 } from "@sablier/v2-core/types/Tokens.sol";

Create a contract called LockupDynamicStreamCreator, and declare a constant DAI of type IERC20 and an immutable variable sablier of type ISablierV2LockupDynamic:

contract LockupDynamicStreamCreator {
IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
ISablierV2LockupDynamic public immutable sablier;
}

In the code above, the address of the DAI stablecoin is hard-coded. However, in production, you would likely use an input parameter for this and pass the input into a memory variable, allowing the contract to change the assets it interacts with on a per transaction basis.

Initialization

To initialize the Sablier contract, you first need to grab its address from the Deployment Addresses page. Once you have obtained it, pass it to the constructor of the creator contract:

constructor(ISablierV2LockupDynamic sablier_) {
sablier = sablier_;
}

Create functions

There are two create functions in the Lockup Dynamic contract:

  • createWithMilestones: uses specific start and end times, as well as a predefined list of segment milestones
  • createWithDeltas: sets the start time to block.timestamp, and calculates the end time and the segment milestones based on the provided deltas

Which one you choose depends upon your use case. In this guide, we will use createWithMilestones.

Function definition

Define a function called createLockupDynamicStream which takes two parameters, amount0 and amount1, and which returns the id of the created stream:

function createLockupDynamicStream(uint256 amount0, uint256 amount1) external 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 also approve the Sablier contract to pull the assets 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(sablier), totalAmount);

For more guidance on how to approve and transfer ERC-20 assets, see this article on the Ethereum website.

Parameters

Sablier uses structs to encode the parameters of its create functions. The struct associated with createWithDurations is LockupDynamic.CreateWithMilestones, and it can be initialized like this:

LockupDynamic.CreateWithMilestones memory params;

Let's review each parameter in detail.

Sender

The address from which to stream the assets, which will have the ability to cancel the stream:

params.sender = msg.sender;

Recipient

The address toward which to stream the assets:

params.recipient = recipient;

Total amount

The total amount of ERC-20 assets to be paid, which includes the stream deposit and any potential fees. This is represented in units of the asset's decimals.

params.totalAmount = totalAmount;

Asset

The contract address of the ERC-20 asset to use for streaming. In this example, we will use DAI:

params.asset = DAI;

Cancelable

Boolean that indicates whether the stream will be cancelable or not.

params.cancelable = true;

Broker

An optional parameter that can be used 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));
info

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.

Start time

The Unix timestamp for when the stream will start.

In this example, the start time is set to 100 seconds into the future:

params.startTime = block.timestamp + 100 seconds;

Segments

Segments are what the protocol uses to compose the custom streaming curve of a Lockup Dynamic stream. For a full exposition of segments, see the Segments guide.

The term "milestones" in the function createWithMilestones refers to the splitting of the stream into several distinct segments, with each segment characterized by a specific amount, exponent, and milestone. These segments are supplied to the function in the form of an array containing LockupDynamic.Segment structs.

Let's define two dummy segments:

params.segments.push(
LockupDynamic.Segment({ amount: amount0, exponent: ud2x18(3.14e18), milestone: block.timestamp + 4 weeks })
);
params.segments.push(
LockupDynamic.Segment({ amount: amount1, exponent: ud2x18(0.5e18), milestone: block.timestamp + 2 years })
);

In this example, the first segment (amount0) will stream much slower than the second segment (amount1), because the exponents are different. As a rule of thumb: the higher the exponent, the slower the stream.

note

The segment milestones must be in ascending order.

info

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 createWithMilestones function, and assign the id of the newly created stream to a variable:

streamId = sablier.createWithMilestones(params);

The complete Lockup Dynamic stream creator contract

Below you can see the complete functioning code: a contract that creates Lockup Dynamic streams with predefined segment milestones. You can access the code on GitHub through this link.

Lockup Dynamic Stream Creator
loading...