RobinFlowStreams
0x3068bf0522d721e674ab9a252fdc50bb614b3cae
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
7,436 bytes
ABI entries
45
11 read · 9 write
License
none
Contract information
- Address
- 0x3068bf0522d721e674ab9a252fdc50bb614b3cae
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0xbFd1DFfd49…435ce0F502
- Creation tx
- 0x915fe0d3c6…8964ac7fe1
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (11)
MAX_FLAT_FEE() → uint256
collectedFees() → uint256
flatFee() → uint256
getStream(uint256) → tuple
incomingStreams(address) → uint256[]
nextStreamId() → uint256
outgoingStreams(address) → uint256[]
owner() → address
streams(uint256) → address, address, address, uint128, uint128, uint128, uint40, uint40, uint40, uint32, bool, bool, bool, string
vestedAmount(uint256, uint40) → uint256
withdrawable(uint256) → uint256
Events (8)
CanceledFeePaidFeesWithdrawnFlatFeeUpdatedOwnershipTransferredRecipientTransferredStreamCreatedWithdrawn
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "owner_",
"type": "address"
},
{
"internalType": "uint256",
"name": "flatFee_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadyCanceled",
"type": "error"
},
{
"inputs": [],
"name": "FeeTooHigh",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientFee",
"type": "error"
},
{
"inputs": [],
"name": "InvalidParams",
"type": "error"
},
{
"inputs": [],
"name": "NoFees",
"type": "error"
},
{
"inputs": [],
"name": "NotCancelable",
"type": "error"
},
{
"inputs": [],
"name": "NotRecipient",
"type": "error"
},
{
"inputs": [],
"name": "NotSender",
"type": "error"
},
{
"inputs": [],
"name": "NotTransferable",
"type": "error"
},
{
"inputs": [],
"name": "NothingToWithdraw",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "TransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "recipientAmount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "senderAmount",
"type": "uint256"
}
],
"name": "Canceled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "payer",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "FeePaid",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "FeesWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "flatFee",
"type": "uint256"
}
],
"name": "FlatFeeUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "oldRecipient",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "RecipientTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint128",
"name": "cliffAmount",
"type": "uint128"
},
{
"indexed": false,
"internalType": "uint40",
"name": "start",
"type": "uint40"
},
{
"indexed": false,
"internalType": "uint40",
"name": "cliff",
"type": "uint40"
},
{
"indexed": false,
"internalType": "uint40",
"name": "end",
"type": "uint40"
},
{
"indexed": false,
"internalType": "uint32",
"name": "period",
"type": "uint32"
},
{
"indexed": false,
"internalType": "string",
"name": "title",
"type": "string"
}
],
"name": "StreamCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
},
{
"indexed": true,
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Withdrawn",
"type": "event"
},
{
"inputs": [],
"name": "MAX_FLAT_FEE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
}
],
"name": "cancel",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "collectedFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint128",
"name": "amount",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "cliffAmount",
"type": "uint128"
},
{
"internalType": "uint40",
"name": "start",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "cliff",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "end",
"type": "uint40"
},
{
"internalType": "uint32",
"name": "period",
"type": "uint32"
},
{
"internalType": "bool",
"name": "cancelable",
"type": "bool"
},
{
"internalType": "bool",
"name": "transferable",
"type": "bool"
},
{
"internalType": "string",
"name": "title",
"type": "string"
}
],
"name": "createStream",
"outputs": [
{
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint128",
"name": "amount",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "cliffAmount",
"type": "uint128"
},
{
"internalType": "uint40",
"name": "start",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "cliff",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "end",
"type": "uint40"
},
{
"internalType": "uint32",
"name": "period",
"type": "uint32"
},
{
"internalType": "bool",
"name": "cancelable",
"type": "bool"
},
{
"internalType": "bool",
"name": "transferable",
"type": "bool"
},
{
"internalType": "string",
"name": "title",
"type": "string"
}
],
"internalType": "struct RobinFlowStreams.StreamParams[]",
"name": "items",
"type": "tuple[]"
}
],
"name": "createStreamsBatch",
"outputs": [
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "flatFee",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
}
],
"name": "getStream",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint128",
"name": "deposited",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "withdrawn",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "cliffAmount",
"type": "uint128"
},
{
"internalType": "uint40",
"name": "start",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "cliff",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "end",
"type": "uint40"
},
{
"internalType": "uint32",
"name": "period",
"type": "uint32"
},
{
"internalType": "bool",
"name": "cancelable",
"type": "bool"
},
{
"internalType": "bool",
"name": "transferable",
"type": "bool"
},
{
"internalType": "bool",
"name": "canceled",
"type": "bool"
},
{
"internalType": "string",
"name": "title",
"type": "string"
}
],
"internalType": "struct RobinFlowStreams.Stream",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "incomingStreams",
"outputs": [
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "nextStreamId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "outgoingStreams",
"outputs": [
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newFee",
"type": "uint256"
}
],
"name": "setFlatFee",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "streams",
"outputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint128",
"name": "deposited",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "withdrawn",
"type": "uint128"
},
{
"internalType": "uint128",
"name": "cliffAmount",
"type": "uint128"
},
{
"internalType": "uint40",
"name": "start",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "cliff",
"type": "uint40"
},
{
"internalType": "uint40",
"name": "end",
"type": "uint40"
},
{
"internalType": "uint32",
"name": "period",
"type": "uint32"
},
{
"internalType": "bool",
"name": "cancelable",
"type": "bool"
},
{
"internalType": "bool",
"name": "transferable",
"type": "bool"
},
{
"internalType": "bool",
"name": "canceled",
"type": "bool"
},
{
"internalType": "string",
"name": "title",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
},
{
"internalType": "address",
"name": "newRecipient",
"type": "address"
}
],
"name": "transferRecipient",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
},
{
"internalType": "uint40",
"name": "t",
"type": "uint40"
}
],
"name": "vestedAmount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
}
],
"name": "withdraw",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "streamId",
"type": "uint256"
}
],
"name": "withdrawable",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {RobinFlowFeeCollector} from "./RobinFlowFeeCollector.sol";
/// @title RobinFlowStreams
/// @notice Token vesting / streaming with competitor-grade schedule options:
/// - optional cliff (time) with an optional initial unlock amount
/// released at the cliff (`cliffAmount`),
/// - linear OR periodic/tranched unlock of the remainder (`period`),
/// - cancelable and recipient-transferable flags,
/// - single or batch creation for many recipients at once.
/// A "token lock" is a stream to yourself with cliff == end.
/// @dev Fee-on-transfer tokens are handled: the escrowed amount is the
/// balance actually received, not the requested amount.
contract RobinFlowStreams is ReentrancyGuard, RobinFlowFeeCollector {
using SafeERC20 for IERC20;
constructor(address owner_, uint256 flatFee_) RobinFlowFeeCollector(owner_, flatFee_) {}
struct Stream {
address sender;
address recipient;
address token;
uint128 deposited;
uint128 withdrawn;
uint128 cliffAmount; // released in full at the cliff
uint40 start;
uint40 cliff;
uint40 end;
uint32 period; // 0/1 = per-second; >1 = unlock every `period` seconds
bool cancelable;
bool transferable;
bool canceled;
string title;
}
/// @notice Parameters for creating a stream (used by single and batch create).
struct StreamParams {
address recipient;
address token;
uint128 amount;
uint128 cliffAmount;
uint40 start;
uint40 cliff;
uint40 end;
uint32 period;
bool cancelable;
bool transferable;
string title;
}
uint256 public nextStreamId = 1;
mapping(uint256 => Stream) public streams;
mapping(address => uint256[]) private _outgoing;
mapping(address => uint256[]) private _incoming;
event StreamCreated(
uint256 indexed streamId,
address indexed sender,
address indexed recipient,
address token,
uint256 amount,
uint128 cliffAmount,
uint40 start,
uint40 cliff,
uint40 end,
uint32 period,
string title
);
event Withdrawn(uint256 indexed streamId, address indexed recipient, uint256 amount);
event Canceled(uint256 indexed streamId, uint256 recipientAmount, uint256 senderAmount);
event RecipientTransferred(uint256 indexed streamId, address indexed oldRecipient, address indexed newRecipient);
error InvalidParams();
error NotRecipient();
error NotSender();
error NotCancelable();
error NotTransferable();
error AlreadyCanceled();
error NothingToWithdraw();
function createStream(
address recipient,
address token,
uint128 amount,
uint128 cliffAmount,
uint40 start,
uint40 cliff,
uint40 end,
uint32 period,
bool cancelable,
bool transferable,
string calldata title
) external payable nonReentrant returns (uint256 streamId) {
_collectFee();
streamId = _createStream(
StreamParams(recipient, token, amount, cliffAmount, start, cliff, end, period, cancelable, transferable, title)
);
}
/// @notice Create many streams in one transaction (batch distribution).
/// Requires a fee of `flatFee` per stream.
function createStreamsBatch(StreamParams[] calldata items)
external
payable
nonReentrant
returns (uint256[] memory ids)
{
if (items.length == 0) revert InvalidParams();
_collectFeeFor(items.length);
ids = new uint256[](items.length);
for (uint256 i = 0; i < items.length; i++) {
ids[i] = _createStream(items[i]);
}
}
function _createStream(StreamParams memory p) private returns (uint256 streamId) {
if (
p.recipient == address(0) ||
p.token == address(0) ||
p.amount == 0 ||
p.end <= p.start ||
p.cliff < p.start ||
p.cliff > p.end ||
p.cliffAmount > p.amount
) revert InvalidParams();
// Measure the amount actually received so fee-on-transfer / rebasing
// tokens are escrowed for exactly what arrived (never over-recorded).
uint256 balBefore = IERC20(p.token).balanceOf(address(this));
IERC20(p.token).safeTransferFrom(msg.sender, address(this), p.amount);
uint256 received = IERC20(p.token).balanceOf(address(this)) - balBefore;
if (received == 0) revert InvalidParams();
uint128 deposited = received > type(uint128).max ? type(uint128).max : uint128(received);
uint128 cliffAmount = p.cliffAmount > deposited ? deposited : p.cliffAmount;
streamId = nextStreamId++;
streams[streamId] = Stream({
sender: msg.sender,
recipient: p.recipient,
token: p.token,
deposited: deposited,
withdrawn: 0,
cliffAmount: cliffAmount,
start: p.start,
cliff: p.cliff,
end: p.end,
period: p.period,
cancelable: p.cancelable,
transferable: p.transferable,
canceled: false,
title: p.title
});
_outgoing[msg.sender].push(streamId);
_incoming[p.recipient].push(streamId);
emit StreamCreated(
streamId, msg.sender, p.recipient, p.token, deposited, cliffAmount, p.start, p.cliff, p.end, p.period, p.title
);
}
/// @notice Amount vested at timestamp `t` (ignores withdrawals/cancel).
/// cliffAmount unlocks in full at the cliff; the remainder vests
/// from `start` to `end`, stepped to `period` boundaries if set.
function vestedAmount(uint256 streamId, uint40 t) public view returns (uint256) {
Stream storage s = streams[streamId];
if (t < s.cliff) return 0;
if (t >= s.end) return s.deposited;
uint256 linearPortion = uint256(s.deposited) - s.cliffAmount;
uint256 elapsed = uint256(t) - s.start;
uint32 per = s.period;
if (per > 1) elapsed = (elapsed / per) * per; // step down to tranche boundary
uint256 linear = (linearPortion * elapsed) / (uint256(s.end) - s.start);
return uint256(s.cliffAmount) + linear;
}
function withdrawable(uint256 streamId) public view returns (uint256) {
Stream storage s = streams[streamId];
if (s.canceled) return 0;
uint256 vested = vestedAmount(streamId, uint40(block.timestamp));
return vested - s.withdrawn;
}
function withdraw(uint256 streamId) external nonReentrant {
Stream storage s = streams[streamId];
if (msg.sender != s.recipient) revert NotRecipient();
uint256 amount = withdrawable(streamId);
if (amount == 0) revert NothingToWithdraw();
s.withdrawn += uint128(amount);
IERC20(s.token).safeTransfer(s.recipient, amount);
emit Withdrawn(streamId, s.recipient, amount);
}
/// @notice Cancel a stream: vested-but-unwithdrawn goes to the recipient,
/// the unvested remainder returns to the sender.
function cancel(uint256 streamId) external nonReentrant {
Stream storage s = streams[streamId];
if (msg.sender != s.sender) revert NotSender();
if (!s.cancelable) revert NotCancelable();
if (s.canceled) revert AlreadyCanceled();
uint256 vested = vestedAmount(streamId, uint40(block.timestamp));
uint256 recipientAmount = vested - s.withdrawn;
uint256 senderAmount = s.deposited - vested;
s.canceled = true;
s.withdrawn = uint128(vested);
if (recipientAmount > 0) IERC20(s.token).safeTransfer(s.recipient, recipientAmount);
if (senderAmount > 0) IERC20(s.token).safeTransfer(s.sender, senderAmount);
emit Canceled(streamId, recipientAmount, senderAmount);
}
function transferRecipient(uint256 streamId, address newRecipient) external {
Stream storage s = streams[streamId];
if (msg.sender != s.recipient) revert NotRecipient();
if (!s.transferable) revert NotTransferable();
if (newRecipient == address(0)) revert InvalidParams();
address old = s.recipient;
s.recipient = newRecipient;
_incoming[newRecipient].push(streamId);
emit RecipientTransferred(streamId, old, newRecipient);
}
function outgoingStreams(address sender) external view returns (uint256[] memory) {
return _outgoing[sender];
}
function incomingStreams(address recipient) external view returns (uint256[] memory) {
return _incoming[recipient];
}
function getStream(uint256 streamId) external view returns (Stream memory) {
return streams[streamId];
}
}
Chain explorer996msChain node76ms