Skip to main content

Batch stream

In this guide, we will show you how to programmatically batch create streams via the proxy and Sablier's ProxyTarget. This is one of the coolest features enabled by the proxy design.

Batch create means creating multiple streams within a transaction, which is done by integrating multiple components:

  • Proxy
  • ProxyTarget
  • Permit2
  • V2 Core
caution

The code in this guide is not production-ready, and is implemented in a simplistic manner for the purpose of learning.

Batch create functions

There are four batch create functions in ProxyTarget, two for each Lockup contract:

We will show to use batchCreateWithDurations in this guide.

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;

As mentioned in the Overview, V2 Periphery uses Permit2 for all token interactions. Permit2 relies on signatures, but in the example below we will use a contract as a stream creator, and contracts cannot sign standard messages. This limitation can be overcome by using EIP-1271.

Let's declare a dummy contract ERC1271 that returns the "magic" EIP-1271 value (the function signature of isValidSignature):

/// @notice Permit2 uses this ERC to validate contract signatures.
contract ERC1271 {
function isValidSignature(bytes32, /* hash */ bytes memory /* signature */ ) public pure returns (bytes4) {
return this.isValidSignature.selector;
}
}

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

import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol";
import { LockupLinear } from "@sablier/v2-core/src/types/DataTypes.sol";
import { ud60x18 } from "@sablier/v2-core/src/types/Math.sol";
import { IERC20 } from "@sablier/v2-core/src/types/Tokens.sol";
import { ISablierV2ProxyTarget } from "@sablier/v2-periphery/src/interfaces/ISablierV2ProxyTarget.sol";
import { Batch, Broker } from "@sablier/v2-periphery/src/types/DataTypes.sol";
import { IAllowanceTransfer, Permit2Params } from "@sablier/v2-periphery/src/types/Permit2.sol";
import { IPRBProxy, IPRBProxyRegistry } from "@sablier/v2-periphery/src/types/Proxy.sol";

Create a contract called BatchLockupLinearStreamCreator that inherits ERC1271, and declare the following constants:

contract BatchLockupLinearStreamCreator is ERC1271 {
IERC20 public constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
IAllowanceTransfer public constant PERMIT2 = IAllowanceTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3);
IPRBProxyRegistry public constant PROXY_REGISTRY = IPRBProxyRegistry(0x584009E9eDe26e212182c9745F5c000191296a78);
ISablierV2LockupLinear public immutable lockupLinear;
ISablierV2ProxyTarget public immutable proxyTarget;
}

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

info

Permit2 and PRBProxyRegistry are deployed at the same address across all chains.

Initialization

To initialize the LockupLinear and the ProxyTarget contracts, you first need to grab their addresses from the Deployment Addresses page. Once you have obtained them, pass them to the constructor of the creator contract:

constructor(ISablierV2LockupLinear lockupLinear_, ISablierV2ProxyTarget proxyTarget_) {
lockupLinear = lockupLinear_;
proxyTarget = proxyTarget_;
}

Function definition

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

function batchCreateLockupLinearStreams(uint256 perStreamAmount) public returns (uint256[] memory streamIds) {
// ...
}

Proxy Steps

Deploy a proxy instance for the creator contract if it doesn't exist:

// Get the proxy for this contract and deploy it if it doesn't exist
IPRBProxy proxy = PROXY_REGISTRY.getProxy({ user: address(this) });
if (address(proxy) == address(0)) {
proxy = PROXY_REGISTRY.deployFor(address(this));
}

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

The caller must approve the creator contract to pull the tokens from the calling address's account. Also, if the allowance is less than the transferred amount, we have to approve the Permit2 contract. To fully benefit from Permit2, we perform a maximum approval:

// Transfer the provided amount of DAI tokens to this contract
DAI.transferFrom(msg.sender, address(this), totalAmount);

// Approve the Permit2 contract to spend DAI
uint256 allowance = DAI.allowance(address(this), address(PERMIT2));
if (allowance < totalAmount) {
DAI.approve({ spender: address(PERMIT2), amount: type(uint256).max });
}

Permit2 Steps

We are going to declare the Permit2 variables needed in the Proxy Target.

// Set up Permit2. See the full documentation at https://github.com/Uniswap/permit2
IAllowanceTransfer.PermitDetails memory permitDetails;
permitDetails.token = address(DAI);
permitDetails.amount = uint160(totalAmount);
permitDetails.expiration = type(uint48).max; // maximum expiration possible
(,, permitDetails.nonce) = PERMIT2.allowance({ user: address(this), token: address(DAI), spender: address(proxy) });

IAllowanceTransfer.PermitSingle memory permitSingle;
permitSingle.details = permitDetails;
permitSingle.spender = address(proxy); // the proxy will be the spender
permitSingle.sigDeadline = type(uint48).max; // same deadline as expiration

// Declare the Permit2 params needed by Sablier
Permit2Params memory permit2Params;
permit2Params.permitSingle = permitSingle;
permit2Params.signature = bytes(""); // dummy signature

Notice that for the signature we passed an empty bytes. This is because contracts can't sign transactions. If we used an EOA instead, a signature would have been required.

For more details on Permit2, see Uniswap's documentation.

Stream Parameters

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

// Declare the first stream in the batch
Batch.CreateWithDurations memory stream0;
stream0.sender = address(proxy); // The sender will be able to cancel the stream
stream0.recipient = address(0xcafe); // The recipient of the streamed assets
stream0.totalAmount = uint128(perStreamAmount); // The total amount of each stream, inclusive of all fees
stream0.cancelable = true; // Whether the stream will be cancelable or not
stream0.durations = LockupLinear.Durations({
cliff: 4 weeks, // Assets will be unlocked only after 4 weeks
total: 52 weeks // Setting a total duration of ~1 year
});
stream0.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined

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

// Declare the second stream in the batch
Batch.CreateWithDurations memory stream1;
stream1.sender = address(proxy); // The sender 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
stream1.durations = LockupLinear.Durations({
cliff: 1 weeks, // Assets will be unlocked only after 1 week
total: 26 weeks // Setting a total duration of ~6 months
});
stream1.broker = Broker(address(0), ud60x18(0)); // Optional parameter left undefined

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

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

Execution

To send the transaction to the proxy, it's necessary to ABI encode the parameters:

bytes memory data = abi.encodeCall(proxyTarget.batchCreateWithDurations, (lockupLinear, DAI, batch, permit2Params));

Once this set-up is complete, we can batch create streams via the proxy and Sablier's ProxyTarget:

bytes memory response = proxy.execute(address(proxyTarget), data);

To return the streamIds, we need to ABI decode the response to uint256[]:

streamIds = abi.decode(response, (uint256[]));

The complete Batch Lockup Linear stream creator contract

Below you can see the complete functioning code: a contract that batch creates Lockup Linear streams via the proxy and Sablier's ProxyTarget that start at block.timestamp. You can access the code on GitHub through this link.

Batch Lockup Linear stream creator
loading...

Lockup Dynamic

To create a Lockup Dynamic stream, the same steps as above apply. The only difference is that we have to declare a different parameter struct and define the segments:

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

You can see a complete functioning example here.