Sablier is a permissionless token distribution protocol for ERC-20 assets. It can be used for vesting, payroll, airdrops, and more. The sender of a payment stream first deposits a specific amount of ERC-20 tokens in a contract. Then, the contract progressively allocates the funds to the recipient, who can access them as they become available over time. The payment rate is influenced by various factors such as the start time, the end time, the total amount of tokens deposited and the type of stream.
Total Pool - $53,440
H/M - $45,000
Low - $3,100
Community Judging Pool - $5,340
Starts: May 10, 2024 Noon UTC
Ends: May 31, 2024 Noon UTC
Sablier is a permissionless token distribution protocol for ERC-20 assets. It can be used for vesting, payroll, airdrops, and more.
The sender of a payment stream first deposits a specific amount of ERC-20 tokens in a contract. Then, the contract progressively allocates the funds to the stream recipient, also known as the Sablier NFT owner, who can access them as they become available over time. The payment rate is influenced by various factors such as the start time, the end time, the total amount of tokens deposited and the type of stream.
There are two repositories:
Periphery contracts interact with core contracts. It allows creation of multiple streams in a single transaction. It also facilitates vesting airdrop distribution.
There are three roles assumed by actors in the Sablier protocol:
Users who are the recipients of the streams. These users own the Sablier NFT which grants them the right to withdraw assets from the stream.
Users who create streams and are responsible for funding them. Senders are also authorized to cancel and renounce streams. These users can also trigger withdrawals on behalf of the recipients but only to the recipient's address.
These are callers who are neither Sender nor Recipient but are allowed to trigger withdrawals on behalf of the recipients. This is because the withdraw function is publicly callable. Note that an unknown caller can withdraw assets only to the recipient's address.
src
├── abstracts
│ ├── Adminable.sol - A minimalist implementation to handle admin access
│ ├── NoDelegateCall.sol - A minimalist implementation to prevent delegate calls
│ └── SablierV2Lockup.sol - Handles common logic between all Sablier V2 Lockup contracts
├── libraries
│ ├── Errors.sol - Library containing all custom errors used in the Sablier protocol
│ ├── Helpers.sol - Helpers to calculate and validate input data required to create streams
│ ├── NFTSVG.sol - Library to generate NFT SVG
│ └── SVGElements.sol - Library to generate specific components of NFT SVG
├── types
│ └── DataTypes.sol - Implementation for a set of custom data types used in V2 core
├── SablierV2LockupDynamic.sol - Creates and manages Lockup streams with a dynamic distribution function
├── SablierV2LockupLinear.sol - Creates and manages Lockup streams with a linear distribution function
├── SablierV2LockupTranched.sol - Creates and manages Lockup streams with a tranched distribution function
└── SablierV2NFTDescriptor.sol - Generates the URI describing the Sablier V2 stream NFTs
src
├── abstracts
│ └── SablierV2MerkleLockup.sol - Handles common logic between all Airstream campaigns
├── libraries
│ └── Errors.sol - Library containing all custom errors used in the Sablier protocol
├── types
│ └── DataTypes.sol - Implementation for a set of custom data types used in V2 periphery
├── SablierV2BatchLockup.sol - Helpers to batch create Sablier V2 Lockup streams
├── SablierV2MerkleLL.sol - Allows users to claim Airdrops using Merkle proofs. These airdrops are powered by Lockup Linear streams
├── SablierV2MerkleLT.sol - Allows users to claim Airdrops using Merkle proofs. These airdrops are powered by Lockup Tranched streams
└── SablierV2MerkleLockupFactory.sol - Factory for deploying Airdrop campaigns using CREATE
Sablier protocol is compatible with the following:
Its not compatible with:
Clone the contest repository:
git clone https://github.com/Cyfrin/2024-05-Sablier.git
cd 2024-05-Sablier
code .
Inside the project directory, run the following commands to install Node.js dependencies and build the contracts:
$ bun install --frozen-lockfile
$ bun run build
To see a list of available scripts:
$ bun run
To run tests:
$ bun run test --no-match-test testFork
To run fork tests against the Ethereum mainnet, follow the below instructions:
.env.example
file to create a .env
fileRPC_URL_MAINNET
to the URL of your mainnet RPC endpoint.Then use the below command to run fork tests:
$ bun run test --match-test testFork
The protocol makes the following assumptions:
The total supply of any ERC-20 token remains below type(uint128).max
.
The transfer
and transferFrom
methods of any ERC-20 token strictly reduce the sender's balance by the transfer amount and increase the recipient's balance by the same amount. In other words, tokens that charge fees on transfers are not supported.
An address's ERC-20 balance can only change as a result of a transfer
call by the sender or a transferFrom
call by an approved address. This excludes rebase tokens and interest-bearing tokens.
The token contract does not allow callbacks (e.g. ERC-777 is not supported).
MAX_SEGMENT_COUNT
and MAX_TRANCHE_COUNT
have values that cannot lead to an overflow of the block gas limit.MerkleLockup
uses a Merkle tree to distribute airstream funds. The leaves of the Merkle tree are not checked to be valid at the contract level. This Merkle tree is generated by the Sablier interface which we expect users to trust. If a user submits Merkle Root directly with an IPFS hash (which we also allow through the UI), our backend performs a validation check to ensure that the Merkle tree is correctly generated.
For MerkleLockup,
, a grace period is defined as the initial period during which clawback
can be used. It ends 7 days after the first airstream claim has been made. Thus, airstream creators are assumed to be trusted during the grace period.
In the case of MerkleLockup
, if an Airstream campaign is created by a team, all members in that team are assumed to have trust among them i.e. they would not rug each other.
In SablierV2MerkleLT
, the unlock percentages and durations for tranches are uniform across all airdrop claimers.
SablierV2Lockup
, when onLockupStreamCanceled()
, onLockupStreamWithdrawn()
and onLockupStreamRenounced()
hooks are called, there could be gas bomb attacks because we do not limit the gas transferred to the external contract. This is intended to support complex transactions implemented in those hooks.In SablierV2MerkleLockupFactory
, users can submit root hashes with duplicate indexes or create Merkle trees with incorrect depths, potentially leading to unclaimable assets or unexpected behavior. These issues are exacerbated by the fact that the Merkle tree's validity is not checked at the contract level. As per assumption (7), it is expected that the Merkle tree will be correctly generated. In case of a malicious Merkle tree, clawback
can be called to withdraw funds from the deployed MerkleLockup
contracts until the grace period ends.
In SablierV2MerkleLockupFactory
, malicious lockupLinear
and lockupTranched
contract addresses can be passed during createMerkleLL()
and createMerkleLT()
functions respectively.
In SablierV2MerkleLockupFactory
, aggregateAmount
and recipientCount
values are exclusively emitted in the CreateMerkleLT
and CreateMerkleLL
events. These values are not validated within the create functions as they are not utilized elsewhere in the contract. However, it's important to note that this impacts integrators who listen to these events and rely on these values. We advise caution, as they may be inaccurate.
If the admin
of the deployed SablierV2MerkleLT
and SablierV2MerkleLL
contracts is modified, the onLockupStreamWithdrawn()
hook callback, if it is implemented, will continue to be made to the original admin
for the airstreams that were already claimed at the time of the change.