Интеграция с Sablier (линейные выплаты)
Стандартный вестинг через смарт-контракт — это cliff + linear unlock: контракт хранит токены, каждые N секунд разблокирует часть. Самописные реализации годами содержат одну и ту же ошибку: расчёт vestedAmount через block.timestamp без учёта того, что транзакция может быть задержана мемпулом. Пользователь делает claim, видит ожидаемую сумму в интерфейсе, отправляет транзакцию — пока она ждёт включения в блок, проходит ещё 30 секунд и фактически полученная сумма отличается. Sablier решает эту задачу через зрелый, проаудированный протокол с детерминированным потоком.
Архитектура Sablier v2
Sablier v2 состоит из двух основных контрактов:
SablierV2LockupLinear — линейные потоки от точки A к точке B. Поддерживает cliff period (период до начала unlock). Стриминг идёт секунда в секунду с момента start time.
SablierV2LockupDynamic — динамические потоки с кастомными сегментами. Можно описать произвольную кривую вестинга: экспоненциальную, stepped, любую комбинацию.
Каждый поток — это NFT (ERC-721). Это ключевое решение Sablier: стрим можно передать другому адресу (transfer stream = transfer право на будущие выплаты), использовать как залог в lending-протоколе или отображать в NFT-маркетплейсе. Sender может отозвать поток (если создан как cancelable) — оставшиеся токены возвращаются.
Создание потоков через контракт
Интеграция на уровне смарт-контракта — для dApp, который создаёт стримы программно (например, DAO выплачивает гранты, протокол начисляет rewards через Sablier вместо 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, // секунды
uint40 totalDuration // секунды
) 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 может отозвать
transferable: false, // NFT нельзя передать (опционально)
durations: LockupLinear.Durations({
cliff: cliffDuration,
total: totalDuration
}),
broker: Broker({ account: address(0), fee: 0 })
});
streamId = sablier.createWithDurations(params);
}
}
Важный момент: totalAmount — это gross amount. Если у токена есть tax, нужно учитывать deflationary transfer и передавать totalAmount с запасом, или использовать transferFrom с проверкой фактического received amount.
Интерфейс управления потоками
Для пользователя нужны:
- Список активных стримов — текущий прогресс, уже выплачено, доступно к клейму, оставшееся
- Withdraw действие — клейм доступных токенов
- Cancel действие (для sender) — отзыв стрима
- Визуализация прогресса — timeline с cliff и linear фазами
Данные для отображения
Sablier предоставляет subgraph (The Graph) для всех основных сетей. Запрос активных стримов для адреса:
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
}
}
Текущий доступный баланс — streamedAmount - withdrawnAmount — считается через on-chain вызов streamedAmountOf(streamId). Subgraph даёт snapshot на момент последнего события, live баланс нужно читать с контракта.
Прогресс-бар с математикой
Для linear стрима без cliff прогресс = (now - startTime) / (endTime - startTime). С cliff — до cliff показываем 0%, после cliff линейная интерполяция. Для dynamic стримов нужно интерполировать по сегментам — SDK Sablier предоставляет 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, // для dynamic streams
});
const progressPercent = Number(streamed * 100n / BigInt(stream.depositAmount));
Сети и деплоиды
Sablier v2 задеплоен на: Ethereum mainnet, Arbitrum, Optimism, Polygon, Avalanche, BNB Chain, Base, Gnosis. Адреса контрактов стабильны — детерминированный деплой через Create2 с одинаковыми адресами на всех сетях (кроме Ethereum mainnet, где адреса немного отличаются).
Типичные use cases
- Team vesting: 4-year vesting с 1-year cliff для команды и инвесторов
- Grant streaming: DAO стримит гранты по месяцам вместо upfront выплат
- Salary in crypto: непрерывный поток USDC как зарплата
- Protocol rewards: замена custom reward-контракта на Sablier streams
Ориентиры по срокам
Базовая интеграция (создание стримов из контракта + UI просмотр) — 3-4 дня. Полноценный интерфейс с управлением, визуализацией и мультисетевой поддержкой — 5-7 дней. Стоимость рассчитывается индивидуально.







