-
Notifications
You must be signed in to change notification settings - Fork 8
Concepts Yield Farming
Yield farming is an interesting concept made popular by many DeFi contracts on the Ethereum ecosystem. It consists in rewarding users who participate in a network by distributing tokens to them, then distributes rewards to users who hold and stake those tokens. The concept is useful because it incentivizes users to partake in the network before the network effect kicks in, and can thereby help bootstrap this network effect. (A network effect is one in which the value of the network per participant increases with the number of participants.)
We will analyze how yield farming works by looking at the implementation of the StakingRewards Solidity contract from Synthetix.
Note that this implementation is not at all designed for gas efficiency: it relies on a lot of calls between contracts, and does not try to compress state. But the underlying principles would remain the same if implemented more efficiently, either in Solidity or in a better language.
Yield farming works between two tokens: a staking token, and a rewards token. Users earn the former by participating in some network; they may then stake those staking tokens, and thereafter, they will receive the latter rewards tokens as a reward over time, based on their relative proportion of the total stake.
In the Synthetix contract StakingRewards contract works between two ERC20's, from two contracts outside the staking reward contract itself. This is somewhat inefficient from a gas perspective, but from an auditing perspective, leverages known contracts.
Because Synthetix works with tickets in separate contracts, updates are expensive and require a separate staking step. This extra staking step could be done away with, with automatic staking, if staking were integrated into the same contract as the tokens.
Note however that a lot of the simplicity in the Synthetix contract is derived from the amounts of staking tokens only changing in discrete events, and being constant in between those events. This means that although the contract involves continuous functions, these functions will all be affine, and simple to compute with the four elementary arithmetic operations.
There are two sets of tokens, a staking token that qualifies holders for rewards,
and a rewards token in which rewards are distributed.
In Synthetix, they are the stakingToken
and rewardsToken
.
At any point in time, there is a reward pool made of rewards tokens being distributed over time. This pool is being distributed every second to all accounts in proportion of their holdings in the staking token, at a rate that only varies at events whereby staking tokens being minted or burned (respectively staked and withdrawn in Synthetix), the reward pool is exhausted, or the reward pool receives new tokens.
Now, it would be totally inefficient to update every account at every change: that would make the cost of every change proportional to the number of accounts. Instead, we want a system that only costs a modest bounded amount of gas when computing the rewards for an account, which can be done on-demand just before an operation affecting the account.
Happily, there exists such a method, as famously made popular by Synthetix.
During a reward period of duration d
(rewardsDuration
in the Synthetix contract),
the contract will be distributing a total reward r
(reward
in the Synthetix contract)
between participants at an overall constant rate r/d
(rewardRate
in the Synthetix contract)
prorated to the stakings of each account.
Imagine a hypothetical staking token that wouldn't change hand from the start of the contract;
at any moment, that token would have accrued across all rate-changing events
some total reward: rewardPerToken
in Synthetix.
Now, between a moment t
and some future moment u
without any rate-changing event in between,
it would have accrued a reward (u-t)*rewardRate/_totalSupply
thereby increasing the rewardPerToken
by as much.
However, regardless of how many times the rate changes, and for how long it stays at any level,
the reward accrued by this token, as well as by any other token,
between the moment t
and the moment u
, is the difference rewardPerToken@u - rewardPerToken@t
between the value of rewardPerToken
at time t
and that at time u
.
Thus, to determine how much an inactive account accrues between two updates,
we only have to remember the rewardPerToken
level at time of last update of that account,
as stored in userRewardPerTokenPaid[account]
,
then compute the difference with the the current rewardPerToken
, and finally
multiply by the number of tokens in that account.
In the Synthetix contract, the value of rewardPerTokenStored
records the reward level as of the lastUpdateTime
,
and increases by rewardRate / _totalSupply
at every update operation
until the periodFinish
is reached.
Due to rounding errors when computing rates,
extra care is required to achieve token-precise accounting.
Notably, there may be some tiny losses of rewards due to rewardPerToken
being rounded down.
Since all tokens use the same rewardPerToken
, there should be no inter-account
discrepancies in computing that integral of rewards over time.
However, note that since this integral is computed by rounding down at each update,
each update causes a tiny loss of rewards for everyone;
by the time the periodFinish
is reached,
there should be a tiny amount left in the rewardPool
that can be carried over for next period,
and that needs to be accounted for.
Moreover, each account update will round down the account's reward by some amount less than one token.
At the scale of 1e-18
per operation, the lossage should be negligible in value,
and remain conservatively in the direction of the a few extra tokens being made unreachable:
not in the rewardPool
anymore, yet never distributed to any particular account.