This repository contains the forge project used to develop, test and deploy the StakedFLIP contracts.
test
├── Aggregator.t.sol | Comprehensive aggregator fuzz tests
├── Burner.t.sol | Comprehensive burn fuzz tests
├── MainMigration.sol | Shared deployment for all tests
├── Minter.t.sol | Comprehensive minter fuzz tests
├── Output.t.sol | Comprehensive output fuzz tests
├── Rebaser.t.sol | Comprehensive rebaser fuzz tests
├── Token.t.sol | Some token tests
├── Upgrade.t.sol | Some upgrade tests
├── Voting.t.sol | Some voting related tests
src
├── mock
│ ├── Flip.sol | Mock ERC20 token used in tests
│ ├── ICurveDeployer.sol | Curve factory interface for tests
│ ├── IStableSwap.sol | Curve Pool interface for Aggregator
│ └── StateChainGateway.sol | Mock of the Chainflip staking contract
├── token
│ ├── stFlip.sol | stFLIP main token
│ └── tStorage.sol | stFLIP's variables
└── utils
├── AggregatorV1.sol | Aggregator implementation
├── BurnerV1.sol | Burner implementation
├── MinterV1.sol | Minter implementation
├── OutputV1.sol | Output implementation
├── Ownership.sol | Shared ownership contract
└── RebaserV1.sol | Rebaser implementation
Chainflip is a cross-chain DEX for performing native cross-chain swaps (swapping ETH -> BTC trustlessly and without wrapping for example). They accomplish this by having a 150 validator network with a threshold signature scheme to control addresses on supported chains. Read more about their project here, although the necessary information to understand StakedFLIP is below. This will start as a high level overview and then become more detailed. StakedFLIP is a liquid staking token for the Chainflip network. Users can stake or purchase (or a combination of the two via the Aggregator!) a greater amount of stFLIP than FLIP given. This FLIP will then be staked to validators. As validators accrue rewards, StakedFLIP will rebase to distribute protocol rewards to holders (stFLIP is always backed 1:1 by stFLIP). There are many similarities to Lido.
This diagram gives a brief overview of how the system works to aid in the understanding of the more detailed description below.
This ultimately is the hub of the protocol. stFLIP is a rebasing and voting token, using OZ's VotesUpgradeable
.
stFLIP has a preSyncSupply
and rewardsToSync
which is updated to match the supply of the FLIP backing. Shares are the actual values stored in the contract storage, and these are converted to actual "balance" in balanceOf
by calculating where the current total supply is in the reward distribution and multiplying by the user's percentage of the shares. VotesUpgradeable
is responsible for the core underlying balance and transfer functionality of the token. For every mint, burn, and transfer, we call transferVotingRights
which creates a new checkpoint with the latest total underlying supply for mints/burns and then creates a new shares checkpoint for from/to addresses. We made some tiny modifications to VotesUpgradeable
to have all addresses delegated to themselves.
After the Rebaser invokes a rebase, preSyncSupply
, rewardsToSync
, syncEnd
and syncStart
which initiates the reward distribution interval which will linearly increase the total supply over time.
stFLIP is a voting token to ensure that staking is non-custodial and that the protocol is in the hand's of the actual users. We use FraxGovernorOmega to have a two-step optimistic governance model where a multi-sig first proposes a given action, for a certain time period, stFLIP holders will then be able to veto the action if it does not align with their desires. Governance is ultimately necessary for a protocol like this since validator addresses will need to be updated, fees dynamics will change, operators will need onboarding, and its likely that the underlying Chainflip protocol experiences significant changes. We see governance tokens for LST protocols to be worse than this model since the actual users still don't have a say. Depending on the action there will be a different proposal time period required. Contract upgrades will be on the much longer end, while whitelisting new validators will be on the shorter end. If users do not like a prospective operator etc, they can veto the action. Governance should not be opt-in since users likely won't see the value of this protection over the additional gas costs. The testing of the Omega deployment is in progress - our use does not require any modifications of Omega.
This is the most simple contract. Users can approve FLIP to be spent by the Minter, and then call mint
which will transfer some amount of FLIP to the Output
and mint the user an equal amount of stFLIP.
This contract handles the stFLIP redemption. Users can burn
some amount of stFLIP to enter the burn queue, and once there is sufficient FLIP in the Output contract a user can redeem
to receive their native FLIP.
Burns are represented in an array of burn_
structures that contain amount
, user
, completed
. After every burn, stFLIP is burnt, a new entry is added to the burns
list with the amount
and recipient
, and amount + sums[sums.length - 1]
is pushed to the sums array, which is a cumulative sum of all the previous burns at a given index. After a redemption, amount
FLIP is transferred to user
and completed
is marked true; redeemed
, a cumulative count of all FLIP that has been claimed, is incremented by amount
. A burn is redeemable if sums[burnId]
(the cumulative sum of all burns prior to and including burnId
) minus redeemed
is less than or equal to the FLIP balance of the Output contract. This invariant ensures that a burn is only eligible to be claimed once there is sufficient FLIP for all previous burns to be fulfilled.
getAllBurns
, getBurns
, getBurnIds
are getter functions used by the frontend. totalPendingBurns
is used by the Rebaser contract.
This is the primary avenue that users will stake/unstake through. After parameters are calculated on the frontend, stakeAggregate
will route the user through Curve and Minter to maximize stFLIP received, and unstakeAggregate
will instant burn (burn + redeem) and burn or sell through Curve depending on whether they accept the possible discount.
This function takes a specified amount of FLIP and swaps a specified amountSwap
and stakes the rest via Minter.
Due to their backing's illiquid nature, liquid staking tokens inherently trade at a discount. Prior to withdrawals, Lido would often sit at a 3-5% discount. Users would still stake normally, which does not make any sense since it is significantly more advantageous to purchase the LST at a discounted rate. The question remains though - what is the optimal amount of LST to purchase for a given pool state. The key idea is that you never want to receive less units of LST than units of native token you are selling. As you continue to purchase more LST the discount will lessen, and eventually disappear. Naively, you could find the maximum amount where average purchase price is less than 1. But, even though your average price is advantageous, there still might be some marginal purchases that are not. Thus, we find that the optimal amount of LST to purchase is the amount where the marginal cost is 1.
Finding this value analytically is non-trivial and unnecessary. We first have _marginalCost
for a given amount
which approximates dy
after a swap for amount
by calculating the slope of the line between points (amount, get_dy(amount))
and (amount + 10**18, get_dy(amount + 10**18))
. We now want to find the amount where marginal cost becomes one - the point at which it is no longer advantageous to purchase more LST.
We find this value via binary search in calculatePurchasable
where we search possible amounts to find an amount where the marginal cost is within targetError
of our targetPrice
. This method has accuracy to < 1 bps easily. If this amountPurchasable
is greater than the amount the user wants to stake, then their entire route will go through Curve.
This function takes a specified amount of stFLIP and instant burns (an atomic burn and redeem) and either normal burns or sells the rest.
The max amount instant burnable is FLIP.balanceOf(output) - totalPendingBurns
. If this exceeds user unstake amount then this will be their entire route. For the potential remainder, they can opt on the frontend to accept the price via Curve or perform a normal burn.
We have spoken with the curve team and they have confirmed that their upcoming stableswap-ng will support rebasing tokens. It is possible we have to adopt the interface for the new pool's specification.
This contract manages all staking operations and keeps a running tally of amount staked/unstaked for use in Rebaser reward calculation. Governance can add operators and whitelist validators. Due to the complexities of Chainflip staking mechanics, we have an offchain service known as the Fund Manager which performs staking operations.
The FLIP token is an erc20 token, but in order to stake it, you must call fundStateChainAccount
on the stateChainGateway
which will take FLIP from an account an issue it to a bytes32
address on the State chain (term for Chainflip network). To unstake (redeem) FLIP, the validator must issue a claim request; the validator network will then sign a threshold signature to registerRedemption
on the stateChainGateway
. One can then executeRedemption
to process and have the FLIP sent to the address specified in the claim.
Chainflip's staking is slightly different than other networks due to the fact that changing validators requires creating rotating keys for the Vaults. The authority (validator) set is determined by an epoch (28 day) long auction period. In the first half of the auction, people are free to fund/redeem from their state chain account but are required to keep their bond. In the second half of the auction, you can only stake to your state chain account. At the end of the epoch, the top 150 validators by stake are selected to be the new authority set. Validators are free to redeem FLIP above the network's minimum active bid for the first half of the next epoch. More info here.
For the foreseeable time after launch, Thunderhead will be the only entity running validators. After we decide to onboard more, we will go through a rigorous vetting process (similar to Lido, which we have gone through) of potential new operators. After selecting new operators, governance can add them via addOperator
, of course, stFLIP holders can reject this if they please. This new operator will be given a number of addresses that they can add via addValidators
. These addresses will not be eligible to be staked to though, since the withdrawal address is specified via a transaction on the statechain. After validators are added, governance must validate that the state chain addresses have the withdrawal address set to the Output to ensure that the contracts remain in control of the funds.
Each operator has a serviceFeeBps
and a validatorFeeBps
, the former is the percentage of the rewards that will go to the StakedFLIP service proviers and the latter is the percentage of rewards that go to the operator.
Near the end of each auction, the fund manager will maximize the number of validators that hit the median active bid (we go above the minimum to ensure all validators are included) while fulfilling all burn requests. The Fund Manager can call fundValidators
with a list of validators and amounts to stake FLIP to whitelisted addresses. The fund manager can also call redeemValidators
with a list of addresses to execute redemptions and claim the unstaked FLIP.
After every stake of amount
to a validator, the validator's operator's staked
counter will increase by amount
. After every unstake of amount
from a validator, the validator's operator's unstaked
counter will increase by amount
. The Rebaser uses these amounts to calculate the amount of rewards earned.
The Rebaser takes in a list of validator addresses and balances from an offchain oracle and adjusts the scaling factor accordingly while distributing fees to operators who generated rewards. The execution goes rebase
-> _updateOperators
-> _updateOperator
-> _validateSupplyChange
-> stflip.setRebase
but we will explain it from the ground up
We must ensure that the calculated supply chain makes sense incase the oracle is faulty. We calculate the APR in case of supply increase and ensure it does not exceed the reasonable APR of the network (~20%). If the supply decreases, we ensure that the decrease is nominal since slashing is rare on Chainflip.
Given an Operator's actual current state chain balance and whether or not a fee should be distributed (this might be disabled if there is a significant slashing), we calculate the rewards generated by comparing their current balance to the staked/unstaked counter found in the Output.
We calculate the principal balance as totalStaked + totalRewards - unstaked - slashCounter
. The reward increment is the actual operator balance minus this value. If negative, we increment the slash counter and continue. If positive, we check that it exceeds the slashCounter
, if it doesn't, we subtract it from slashCounter
. If it is, we subtract the existing slashCounter
from the reward increment and set slashCounter
to zero.* We then increment the service and operator pending fee by this value multiplied by the fee.
** In this way, totalRewards
is a slight misnomer because it does not encapsulate total rewards earned. We do this though to ensure that an operator is not earning fees if they have a slashed deficit.
This takes an array of validators and their balances. It queries the Output contract for the validators Id's and countable status and confirms that the hash of the inputted validators equals the values of the addressses in the contract. It loops through these values to find the total operator balance and then calls _updateOperator
for each operator.
After updating all operators and summing the balances of all validators, totalSupply = stateChainBalance + flip.balanceOf(Output) - burner.totalPendingBurns() - servicePendingFee - totalOperatorPendingFee
. The amount that hasn't been claimed by operators or users who have burned must be subtracted from the amount of FLIP within the system.
An offchain oracle that we will run queries all the validator addresses on the Output, queries their balance on the statechain, and submits this data via rebase
to the Rebaser. This function ultimately adjusts the supply and distributes rewards to operators and the service via pending fees. The supply validation ensures safety in the case of oracle compromise. As Chainflip becomes more mature we will look to onboard with something like Chainlink.
The manager or fee recipient of an operator can claimFee
to receive an amount
or max
of their pendingFee
in either FLIP or stFLIP. After receiving their funds, pendingFee
is decreased.
This gives a solid explanation of the StakedFLIP protocol. Please reach out if you have further questions.
The ABIs are in abi/
The file structure is abi/<ContractName>.json
. For example abi/Aggregator.json
is the ABI of the Aggregator
contract.
The script filter_abi.py
reads through the out
folder created by Foundry during the build process. As the files created during the build process contain significant redundant information, and change on every build, it does not make sense to push these to GitHub. Please run filter_abi.py
after making contract changes and prior to committing. This can be made into an action or commit hook later.