-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: staking contract guide * refactor: polish staking guide * refactor: polish staking docs * refactor: rename tokenId to streamid * refactor: minor rewordings --------- Co-authored-by: Paul Razvan Berg <[email protected]>
- Loading branch information
1 parent
75da54c
commit 81bf4e2
Showing
12 changed files
with
453 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
--- | ||
draft: true | ||
sidebar_position: 7 | ||
sidebar_position: 8 | ||
id: "frontend" | ||
title: "Frontend Integrations" | ||
--- | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
--- | ||
id: "snapshot-voting" | ||
sidebar_position: 8 | ||
sidebar_position: 9 | ||
title: "Snapshot Strategies" | ||
--- | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
--- | ||
id: "etherscan" | ||
sidebar_position: 9 | ||
sidebar_position: 10 | ||
title: "Etherscan" | ||
--- | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
--- | ||
id: "custom-deployments" | ||
sidebar_position: 10 | ||
sidebar_position: 11 | ||
title: "Custom Deployments" | ||
--- | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
--- | ||
id: "stake-setup" | ||
sidebar_position: 1 | ||
title: "Introduction" | ||
--- | ||
|
||
# Staking in Sablier | ||
|
||
Staking is a popular concept in DeFi. A major advantage of using Sablier is that you can set up staking of tokens being | ||
vested through Sablier streams. This is enabled by [hooks](/concepts/protocol/hooks), about which you can read more in | ||
the [hooks guide](/contracts/v2/guides/hooks). | ||
|
||
This series will guide you through an example of a staking contract with the following features: | ||
|
||
1. Allowing users to stake Sablier streams for a particular token. | ||
2. Earning and claiming rewards in the same token as the vested token. | ||
3. Allowing users to unstake their staked Sablier streams. | ||
|
||
:::warning | ||
|
||
The code provided in this guide has NOT BEEN AUDITED and is provided "AS IS" with no warranties of any kind, either | ||
express or implied. It is intended solely for demonstration purposes. | ||
|
||
::: | ||
|
||
## Assumptions | ||
|
||
Before diving in, please note that we will make the following assumptions: | ||
|
||
1. The guide demonstrates staking of only one type of stream at a time, either Lockup Dynamic or Lockup Linear. This is | ||
because each type is a different contract. You can build your own contract to stake all types of streams. | ||
1. Since staking requires transferring the Sablier NFT from users' wallet to the staking contract, the Sablier stream | ||
must be transferable at the time of creation. | ||
1. The staking contract allows staking of one stream per user. So, if a user has already staked a stream, he will not be | ||
able to stake another stream from the same address. This is assumed for simplicity's sake. | ||
1. Rewards are distributed at a fixed rate, for a fixed duration, and are bound by an end time. | ||
|
||
## First steps | ||
|
||
Let's begin with the constructor. | ||
|
||
Create a contract called `StakeSablierNFT` and write the `constructor` as follows: | ||
|
||
```solidity | ||
contract StakeSablierNFT is | ||
Adminable, | ||
ERC721Holder, | ||
ISablierLockupRecipient // Required to implement hooks | ||
{ | ||
constructor(address initialAdmin, IERC20 rewardERC20Token_, ISablierV2Lockup sablierLockup_) { | ||
admin = initialAdmin; | ||
rewardERC20Token = rewardERC20Token_; | ||
sablierLockup = sablierLockup_; | ||
} | ||
} | ||
``` | ||
|
||
As mentioned above, a user will only be able to stake a stream that is vesting tokens specified by `rewardERC20Token_` | ||
in the constructor. The rewards will also be distributed in the same token. | ||
|
||
:::info | ||
|
||
`ISablierV2Lockup` is a shared interface between `ISablierV2LockupLinear`, `ISablierV2LockupDynamic` and | ||
`ISablierV2LockupTranched`, allowing users to interact with either contract type using a single interface. | ||
|
||
::: | ||
|
||
To focus on specific functionalities that enable staking support for streams, obvious functions such as | ||
`startStakingPeriod` have been omitted from this guide. However, for completeness, the full code can be found on the | ||
next page as well as in the | ||
[examples repo](https://github.com/sablier-labs/examples/blob/main/v2/core/StakeSablierNFT.sol). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
--- | ||
id: "stake-full-code" | ||
sidebar_position: 2 | ||
title: "Full code" | ||
--- | ||
|
||
The guide in the following pages will cover each of the essential functionalities of the `StakeSablierNFT` contract. For | ||
those who want to start hacking right away, here is the full code, which can also be found on | ||
[GitHub](https://github.com/sablier-labs/examples/blob/main/v2/core/StakeSablierNFT.sol): | ||
|
||
```solidity reference title="StakeSablierNFT contract" | ||
https://github.com/sablier-labs/examples/blob/main/v2/core/StakeSablierNFT.sol | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
--- | ||
id: "stake-hooks" | ||
sidebar_position: 3 | ||
title: "Hooks" | ||
--- | ||
|
||
As explained in the [access control](/contracts/v2/reference/access-control#overview) section, the Sablier Protocol | ||
allows anyone to trigger withdrawals from a stream. For the staking contract, we want to make sure that any call to | ||
`withdraw` also updates the states of the staking contract. So in this section, we will discuss how we can create such | ||
control flows with Sablier hooks. | ||
|
||
Hooks enable callbacks to the staking contract in the following scenario: | ||
|
||
1. A call to `cancel` or `withdraw` function is made. | ||
2. The staking contract is the recipient of the Sablier stream. | ||
|
||
Depending on your requirement, you can implement custom logic to be executed when the sender cancels or a user withdraws | ||
from a staked stream. For example, you might want to automatically unstake the stream if `cancel` is called, or you | ||
might want to update the internal accounting if `withdraw` is called. Hooks make that happen. | ||
|
||
For this example, we will implement the following logic: | ||
|
||
1. When a stream is canceled, update the user's staked balance in the staking contract. | ||
2. When a withdrawal is made, update the user's staked balance in the staking contract and transfer the withdrawn amount | ||
to the user's address. | ||
|
||
:::note | ||
|
||
A dedicated guide for hooks is available [here](/contracts/v2/guides/hooks). | ||
|
||
::: | ||
|
||
### Cancel hook | ||
|
||
```solidity | ||
/// @notice Implements the hook to handle cancelation events. This will be called by Sablier contract when a stream | ||
/// is canceled by the sender. | ||
/// @dev This function subtracts the amount refunded to the sender from `totalERC20StakedSupply`. | ||
/// - This function also updates the rewards for the staker. | ||
function onSablierLockupCancel( | ||
uint256 streamId, | ||
address, /* sender */ | ||
uint128 senderAmount, | ||
uint128 /* recipientAmount */ | ||
) | ||
external | ||
updateReward(stakedUsers[streamId]) | ||
returns (bytes4 selector) | ||
{ | ||
// Check: the caller is the Lockup contract. | ||
if (msg.sender != address(sablierLockup)) { | ||
revert UnauthorizedCaller(msg.sender, streamId); | ||
} | ||
// Effect: update the total staked amount. | ||
totalERC20StakedSupply -= senderAmount; | ||
return ISablierLockupRecipient.onSablierLockupCancel.selector; | ||
} | ||
``` | ||
|
||
### Withdraw hook | ||
|
||
```solidity | ||
/// @notice Implements the hook to handle withdraw events. This will be called by Sablier contract when withdraw is | ||
/// called on a stream. | ||
/// @dev This function transfers `amount` to the original staker. | ||
function onSablierLockupWithdraw( | ||
uint256 streamId, | ||
address, /* caller */ | ||
address, /* recipient */ | ||
uint128 amount | ||
) | ||
external | ||
updateReward(stakedUsers[streamId]) | ||
returns (bytes4 selector) | ||
{ | ||
// Check: the caller is the Lockup contract | ||
if (msg.sender != address(sablierLockup)) { | ||
revert UnauthorizedCaller(msg.sender, streamId); | ||
} | ||
address staker = stakedUsers[streamId]; | ||
// Check: the staker is not the zero address. | ||
if (staker == address(0)) { | ||
revert ZeroAddress(staker); | ||
} | ||
// Effect: update the total staked amount. | ||
totalERC20StakedSupply -= amount; | ||
// Interaction: transfer the withdrawn amount to the original staker. | ||
rewardERC20Token.safeTransfer(staker, amount); | ||
return ISablierLockupRecipient.onSablierLockupWithdraw.selector; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
--- | ||
id: "stake-stake" | ||
sidebar_position: 4 | ||
title: "Stake" | ||
--- | ||
|
||
The `stake` function takes the `streamId` as the input, and stakes the stream by transferring the Sablier NFT from the | ||
user's wallet to the staking contract. | ||
|
||
```solidity | ||
function stake(uint256 streamId) external { | ||
// code goes here. | ||
} | ||
``` | ||
|
||
As the first step, we will check if the underlying token being vested through the stream is same as the reward token. | ||
Since the Sablier protocol can be used to stream any ERC-20 token, this check ensures that the staking contract does not | ||
accept unknown tokens. | ||
|
||
```solidity | ||
if (sablierLockup.getAsset(streamId) != rewardERC20Token) { | ||
revert DifferentStreamingToken(streamId, rewardERC20Token); | ||
} | ||
``` | ||
|
||
As mentioned in the assumptions, the contract only allows staking one NFT at a time. So, we will now check if the user | ||
is already staking. | ||
|
||
```solidity | ||
if (stakedStreams[msg.sender] != 0) { | ||
revert AlreadyStaking(msg.sender, stakedStreams[msg.sender]); | ||
} | ||
``` | ||
|
||
Finally, we will set some storage variables and transfer the NFT: | ||
|
||
```solidity | ||
stakedUsers[streamId] = msg.sender; | ||
stakedStreams[msg.sender] = streamId; | ||
totalERC20StakedSupply += _getAmountInStream(streamId); | ||
sablierLockup.safeTransferFrom({ from: msg.sender, to: address(this), tokenId: streamId }); | ||
``` | ||
|
||
The `_getAmountInStream` function retrieves the amount of tokens being vested through the stream. | ||
|
||
```math | ||
\text{amount in a stream} = (\text{amount deposited} - \text{amount withdrawn} - \text{amount refunded}) | ||
``` | ||
|
||
The implementation is as follows: | ||
|
||
```solidity | ||
/// @dev The following function determines the amounts of tokens in a stream irrespective of its cancelable status. | ||
function _getAmountInStream(uint256 streamId) private view returns (uint256 amount) { | ||
// The tokens in the stream = amount deposited - amount withdrawn - amount refunded. | ||
return sablierLockup.getDepositedAmount(streamId) - sablierLockup.getWithdrawnAmount(streamId) | ||
- sablierLockup.getRefundedAmount(streamId); | ||
} | ||
``` |
Oops, something went wrong.