Skip to main content

Create a Batch of Lockup Dynamic Streams

In this guide, we will show you how to programmatically batch create dynamic streams via the Sablier's Batch contract.

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


This guide interacts with the Periphery contract.


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

Now, import the relevant symbols from @sablier/v2-core and @sablier/v2-periphery:

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 { ISablierV2LockupDynamic } from "@sablier/v2-core/src/interfaces/ISablierV2LockupDynamic.sol";
import { ISablierV2Batch } from "@sablier/v2-periphery/src/interfaces/ISablierV2Batch.sol";
import { Batch, Broker, LockupDynamic } from "@sablier/v2-periphery/src/types/DataTypes.sol";

Create a contract called BatchLockupDynamicStreamCreator, and declare a constant DAI of type IERC20, a constant LOCKUP_DYNAMIC of type ISablierV2LockupDynamic, and a constant BATCH of type ISablierV2Batch:

contract BatchLockupDynamicStreamCreator {
IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
ISablierV2LockupDynamic public constant LOCKUP_DYNAMIC =
ISablierV2Batch public constant BATCH = ISablierV2Batch(0xEa07DdBBeA804E7fe66b958329F8Fa5cDA95Bd55);

In the code above, the contract addresses are hard-coded for demonstration purposes. However, in production, you would likely use input parameters to allow flexibility in changing the addresses.

Also, these addresses are deployed on Mainnet. If you need to work with a different chain, the Sablier addresses can be obtained from the Deployment Addresses page.

Batch create functions

There are two batch create functions for the Lockup Dynamic contract:

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

Function definition

Define a function called batchCreateStreams that takes a parameter perStreamAmount and returns an array of ids for the created streams:

function batchCreateStreams(uint128 perStreamAmount) public returns (uint256[] memory streamIds) {
// ...

Batch size

Next, declare a batch size, which is needed to calculate the transfer amount:

// Create a batch of two streams
uint256 batchSize = 2;

// Calculate the combined amount of DAI assets to transfer to this contract
uint256 transferAmount = perStreamAmount * batchSize;

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 Batch 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), transferAmount);

// Approve the Batch contract to spend DAI
DAI.approve(address(BATCH), transferAmount);

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

Stream Parameters

Given that we declared a batchSize of two, we need to define two Batch.CreateWithMilestones structs:

// Declare the first stream in the batch
Batch.CreateWithMilestones memory stream0;
stream0.sender = address(0xABCD); // The sender to stream the assets, he will be able to cancel the stream
stream0.recipient = address(0xCAFE); // The recipient of the streamed assets
stream0.totalAmount = perStreamAmount; // The total amount of each stream, inclusive of all fees
stream0.cancelable = true; // Whether the stream will be cancelable or not = Broker(address(0), ud60x18(0)); // Optional parameter left undefined

// Declare some dummy segments
stream0.segments = new LockupDynamic.Segment[](2);
stream0.segments[0] = LockupDynamic.Segment({
amount: uint128(perStreamAmount / 2),
exponent: ud2x18(0.25e18),
milestone: uint40(block.timestamp + 1 weeks)
stream0.segments[1] = (
amount: uint128(perStreamAmount - stream0.segments[0].amount),
exponent: ud2x18(2.71e18),
milestone: uint40(block.timestamp + 24 weeks)

To add some variety, we will change the parameters of the second stream:

Batch.CreateWithMilestones memory stream1;
stream1.sender = address(0xABCD); // The sender to stream the assets, he will be able to cancel the stream
stream1.recipient = address(0xBEEF); // The recipient of the streamed assets
stream1.totalAmount = uint128(perStreamAmount); // The total amount of each stream, inclusive of all fees
stream1.cancelable = false; // Whether the stream will be cancelable or not = Broker(address(0), ud60x18(0)); // Optional parameter left undefined

// Declare some dummy segments
stream1.segments = new LockupDynamic.Segment[](2);
stream1.segments[0] = LockupDynamic.Segment({
amount: uint128(perStreamAmount / 4),
exponent: ud2x18(1e18),
milestone: uint40(block.timestamp + 4 weeks)
stream1.segments[1] = (
amount: uint128(perStreamAmount - stream1.segments[0].amount),
exponent: ud2x18(3.14e18),
milestone: uint40(block.timestamp + 52 weeks)

Once both structs are declared, the batch array has to be filled:

// Fill the batch array
Batch.CreateWithMilestones[] memory batch = new Batch.CreateWithMilestones[](batchSize);
batch[0] = stream0;
batch[1] = stream1;

Invoke the batch create function

With all parameters set, we can now call the createWithMilestones function, and assign the ids of the newly created streams to the array:

streamIds = BATCH.createWithMilestones(LOCKUP_DYNAMIC, DAI, batch);

The complete Batch Lockup Dynamic stream creator contract

Below you can see the complete functioning code: a contract that batch creates Lockup Dynamic streams using Sablier's Batch that start at block.timestamp. You can access the code on GitHub through this link.

Batch Lockup Dynamic stream creator