Integration with Sablier (linear payments)
Standard vesting through smart contract is cliff + linear unlock: contract holds tokens, every N seconds unlocks a portion. Custom implementations for years contain one common mistake: calculating vestedAmount via block.timestamp without accounting for transaction delay in mempool. User makes claim, sees expected amount in UI, sends transaction — while waiting for block inclusion, another 30 seconds pass and actually received amount differs. Sablier solves this via mature, audited protocol with deterministic stream.
Sablier v2 architecture
Sablier v2 consists of two main contracts:
SablierV2LockupLinear — linear streams from point A to point B. Supports cliff period (period before unlock starts). Streaming goes second by second from start time.
SablierV2LockupDynamic — dynamic streams with custom segments. Can describe arbitrary vesting curve: exponential, stepped, any combination.
Each stream is an NFT (ERC-721). This is Sablier's key decision: stream can be transferred to another address (transfer stream = transfer right to future payments), used as collateral in lending protocol or displayed in NFT marketplace. Sender can revoke stream (if created as cancelable) — remaining tokens return.
Creating streams through contract
Integration at smart contract level — for dApp creating streams programmatically (e.g., DAO pays grants, protocol accrues rewards via Sablier instead of custom vesting).
import { ISablierV2LockupLinear } from "@sablier/v2-core/interfaces/ISablierV2LockupLinear.sol";
import { LockupLinear, Broker } from "@sablier/v2-core/types/DataTypes.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract VestingManager {
ISablierV2LockupLinear public immutable sablier;
IERC20 public immutable token;
constructor(address _sablier, address _token) {
sablier = ISablierV2LockupLinear(_sablier);
token = IERC20(_token);
}
function createVestingStream(
address recipient,
uint128 totalAmount,
uint40 cliffDuration, // seconds
uint40 totalDuration // seconds
) external returns (uint256 streamId) {
token.approve(address(sablier), totalAmount);
LockupLinear.CreateWithDurations memory params = LockupLinear.CreateWithDurations({
sender: address(this),
recipient: recipient,
totalAmount: totalAmount,
asset: token,
cancelable: true, // DAO can revoke
transferable: false, // NFT cannot be transferred (optional)
durations: LockupLinear.Durations({
cliff: cliffDuration,
total: totalDuration
}),
broker: Broker({ account: address(0), fee: 0 })
});
streamId = sablier.createWithDurations(params);
}
}
Important: totalAmount is gross amount. If token has tax, need to account for deflationary transfer and pass totalAmount with reserve, or use transferFrom with actual received amount check.
Stream management interface
For user need:
- List of active streams — current progress, already paid, available to claim, remaining
- Withdraw action — claim available tokens
- Cancel action (for sender) — revoke stream
- Progress visualization — timeline with cliff and linear phases
Data for display
Sablier provides subgraph (The Graph) for all major networks. Query active streams for address:
query GetStreams($recipient: String!) {
streams(
where: { recipient: $recipient, status: STREAMING }
orderBy: startTime
orderDirection: desc
) {
id
asset { symbol, decimals }
depositAmount
withdrawnAmount
startTime
endTime
cliffTime
cancelable
transferable
}
}
Current available balance — streamedAmount - withdrawnAmount — calculated via on-chain call streamedAmountOf(streamId). Subgraph gives snapshot at last event, live balance needs to be read from contract.
Progress bar with math
For linear stream without cliff, progress = (now - startTime) / (endTime - startTime). With cliff — show 0% until cliff, then linear interpolation. For dynamic streams need to interpolate by segments — Sablier SDK provides computeStreamedAmount helper.
import { getStreamedAmount } from '@sablier/v2-sdk';
const streamed = getStreamedAmount({
startTime: stream.startTime,
endTime: stream.endTime,
depositAmount: BigInt(stream.depositAmount),
cliffTime: stream.cliffTime,
currentTime: BigInt(Math.floor(Date.now() / 1000)),
segments: stream.segments, // for dynamic streams
});
const progressPercent = Number(streamed * 100n / BigInt(stream.depositAmount));
Networks and deployments
Sablier v2 deployed on: Ethereum mainnet, Arbitrum, Optimism, Polygon, Avalanche, BNB Chain, Base, Gnosis. Contract addresses stable — deterministic deployment via Create2 with same addresses across all networks (except Ethereum mainnet where addresses slightly differ).
Typical use cases
- Team vesting: 4-year vesting with 1-year cliff for team and investors
- Grant streaming: DAO streams grants monthly instead of upfront payments
- Salary in crypto: continuous USDC stream as salary
- Protocol rewards: replacement of custom reward contract with Sablier streams
Timeline estimates
Basic integration (creating streams from contract + UI viewing) — 3-4 days. Full interface with management, visualization and multi-network support — 5-7 days. Cost calculated individually.







