Nyacademy: Unrugged // How Yield Farming Contract Works
And… The Case of the ‘Missing’ Tokens (💧x🐱)
This episode, we are going to take a look at the popular yield farming contract UniswapRewardsFactory and the SmartChef (somewhat ‘MasterChef’) contract.
If you follow us close enough, you will realize that we had a recent collaboration with Bliquid ($BLIQ) on a Nyanchpool which got delayed by a day. We’ve never had such issues before, so something interesting must have happened.
We thought that we could go through the yield farming contracts and also add in a twist of the recent BLIQ token encounters. So we’re going to share our experience on hosting a field farm with self-burning mechanism (like BLIQ) using UniswapRewardsFactory contract. This probably won’t affect you if you’re not using the UniswapRewardsFactory.
What is UniswapRewardsFactory
UniswapRewardsFactory is a reward distribution contract (yield farming) pioneered by Uniswap. It is a super strict contract that absolutely makes sure that the predefined expected rewards are exactly the same as the actual rewards.
If you follow how we do our collaboration transactions. We don’t ask our partners to send their tokens to our Deployer. The reason is that Deployers are usually EOA (just a wallet account) so anyone (like the host, aka us) with the keys can simply steal the huge amount of tokens sent by the partners. Here’s a look at PancakeSwap’s Deployer.
Could you imagine how much trust is needed for someone to send that many tokens to an EOA account? Of course, PancakeSwap is very trustworthy, but we can’t speak the same for others (like us 🐱). So we’re preventing you.. from us. We usually ask our partners to send the tokens directly to the RewardsFactory contract. This way, we can’t steal the tokens.
So.. Why was the BLIQ Nyanchpool delayed?
It’s solely mathematical. The interesting thing about BLIQ token is that it has a self-burning mechanism whenever someone triggers a transfer of the token. In short, 6% would be taken out of the sent amount, leaving 94% at the destination. Is that a big deal? Apparently for us, huge deal.
If you’re hosting a normal ERC20 token, there won’t be such issues at all.
How Does the UniswapRewardsFactory Work?
Two contracts are in the picture. The UniswapRewardsFactory (referred to as Factory) and the UniswapRewards (referred to as Reward).
We are distributing 21 BLIQ over 7 days on our Nyanchpool.
- Bliquid sends 21 BLIQ to Factory
- Nyanswop triggers ‘notifyRewards’ function on Factory
- Factory sends 21 BLIQ to Reward
- Reward calculates predefined expected rewardRate against actual rewardRate, and make sure the expected rewardRate is ≤ actual rewardRate (to ensure that it has sufficient BLIQ to pay out to stakers)
- If all passes, the pool will start.
There is no interaction with the Reward contract. We only interact with the Factory contract.
RewardRate is defined as BLIQ balance / 604800 (7 days in seconds).
So expected RewardRate is fixed at 21 / 604800, which is 0.000034722.
And actual RewardRate depends on the BLIQ balance of the Rewards contract divded by 604800, which cannot be calculated now.
Ideally it should be exactly the same. And you thought so..
*note that the real representation of 21 is 21*10¹⁸ (18 zeros), we’re shortening it to just 21 for readability
Introducing RewardsFactory (and Self-Burning Token)
Step 1 — Receive 21 BLIQ in Factory Contract 😸
It starts with Bliquid sending the agreed 21 BLIQ to the Factory contract.
Cheers. We’re set. Let’s call the deployment function~
Step 2 — Trigger notifyRewards function in Factory Contract 🐱
Our first deployment attempt was hit with an error. The error simply means that a transfer has failed. We failed at ‘Step 1’. Though we’re pretty sure we saw BscScan showing that the Factory contract has 21 BLIQ.. but is it really 21 BLIQ?
‘Thanks’ BscScan, you lied. That was the moment we recalled that BLIQ has a self-burning mechanism. The awesome team at Bliquid reacted instantly and sent over a small bit of tokens to make it 21. So we try again, we trigger ‘Step 2’ again.
Step 3 — Reward to Receive 21 BLIQ from Factory Contract 😾
This error is new. It is something we haven’t encountered before even after hosting 4 projects on Nyanchpool. So we went to find which code threw this error.
Step 4 — Pass Expected vs Actual rewardRate Integrity Check 🙀
We made it through ‘Step 3’ but failed at ‘Step 4’. At ‘Step 3’, Factory sends the BLIQ to Reward so by right it shouldn’t fail. Here we recall that the expected
rewardRate is fixed which is 0.000034722 (21 / 604800), so the only variable is the actual balance
rewardRate (line 588).
So something clicked. Since the transferred BLIQ will lose 6% when it reaches the Reward contract, we’ll need our Factory to have at least 22.3404255319 BLIQ (21 / 0.94), so that we will end up with at least 21 BLIQ at Reward after the transfer from Factory. So.. we got Bliquid (thanks again) to send us a bit more BLIQ. Now, we have slighly more than 22.3404255319 BLIQ in our Factory contract so that should finally work.
But we seem to hit the same issue again. Seems like we somehow still don’t have 21 BLIQ to pass ‘Step 4’. The real cause might not be as what we thought. We could, logically speaking, send more BLIQ to Factory contract, or dig out the real cause. We chose to believe in our previous judgements, and dig into the possibilities. And we found something that we overlooked:
In the Factory contract, it will always and only send the predefined rewardAmount which is 21. Sending more than 21 BLIQ doesn’t help as the contract will only send 21 BLIQ. So that means we’re always going to get 19.74 BLIQ (94% of 21) in the Reward contract no matter what.
So does that means that the 21 BLIQ is permanently ‘stuck’ in the Factory contract and that we’re never ever able to host projects with self-burning mechanism?
require(rewardRate <= balance.div(rewardsDuration), "Provided reward too high");
After spending some time trying to balance this equation.. Yes, by the looks of it, it’s pretty much not possible unless we make modifications to the contract in the future. Looks like the 21 BLIQ is stuck. So we disappointedly posted to stop the BLIQ Nyanchpool.
*inserts sad cat* 😿
After spending the whole night looking for possibilities to transfer out the 21 BLIQ. We found a workaround to make the BLIQ Nyanchpool happen.
After ‘Step 3’ (sending 21 BLIQ from Factory to Reward contract), the actual rewardRate’s balance is calculated by
rewardsToken.balanceOf(address(this)) , which means the ‘current’ BLIQ in the Reward contract, which is ‘currently’ 19.74 BLIQ.
So.. what if we send an extra 1.26 BLIQ to our Rewards contract, will we hit 21 BLIQ at this line?
Yes nya~ it worked. From the ‘Tokens Tranferred’, you can see that only 19.74 BLIQ got sent to our Reward contract (as expected, 94%), and the rest was ‘burn’ away. Based on BLIQ’s token behavior, 1% will be burned, 5% will be sent back to the BLIQ contract for some processing.
And… we’re back~
That’s how the UniswapRewardsFactory works for rewards distribution. Hopefully that gives you the basic know-how on how it actually works. Having such knowledge is very beneficial especially when you’re checking on a newly designed contract.
Are Other Farming Contracts Able to Support Self-Burning Tokens As-is?
For yield farming contracts, we should always look at how the contract determines the start and end of the farming period.
Let’s take a quick look at PancakeSwap’s SmartChef contract.
What’s Initialized When the Contract is Deployed?
For starters, we should always look at the constructor first.
The contract seems pretty standard for yield farming contract. Mainly, it initalizes the
end of the farming period. For SmartChef, the fixed
rewardPerBlock is also initialized here.
Similarly, UniswapRewards contract constructor initializes the same thing. The difference is there UniswapRewards is created by triggering a function in the UniswapRewardsFactory.
How Does It Determines Start and End of Distribution?
For UniswapRewards contract, there is check (mentioned in the BLIQ scenario):
require(rewardRate <= balance.div(rewardsDuration), "Provided reward too high");
It is done upon the triggering of the start of the rewards distribution (notifyRewards). So we can look at how SmartChef determines that a reward distribution has started. One unique aspect of the ‘MasterChef’ style contract is the function to calculate the rewards for the users, usually named as the
Line 803 determines if a reward distribution has started. If the current time is before the predefined starting block, this function will exit immediately.
Line 812 calculates the CAKE reward for for user based on the percentage ownership of the pool multiplied by a predefined rewardPerBlock rate.
So how does the SmartChef contract determines the end of a reward distribution phase? For this, we can follow the
bonusEndBlock variable initialized in the constructor. That will lead to just one part of the contract, the
If you prefer to the previous image, you can see that the
_from is the
_to is the current block number.
_from will keep on getting updated with the current block number (check previous image, Line 814). There are two places in the code that uses this function, and both made checks to ensure that
block.number is always > than
pool.lastRewardBlock . Of course, these values will change over time, but we’re just looking at this point for starting and ending point of view.
We’re looking for how it determines ending, so
0 seems to be decent candidate. So the code will hit at Line 780 when the
lastRewardBlock eventually becomes ≥ than the
bonusEndBlock , as it will keep updating to become the value of current number. So, in a way, the SmartChef contract doesn’t end, it just reduces the rewards to 0.
There’s no strict check on whether the SmartChef is able to distribute the predefined rewards when the rewards distribution starts. You have to really trust the owner to do the right thing. The UniswapRewards contract, however, checks that before allowing the reward distribution to commerce.
That also means that SmartChef, technically speaking, could start the rewards distribution even without having 100% of the pre-stated reward token amount. Consider this following:
- SmartChef is to distribute 1000 NYA tokens over 100 blocks.
- SmartChef is initialized with:
So 10 NYA would be distributed every block from 500 to 600. But it doesn’t guarantee that it is able to distribute all of it at any point of time (especially during the start). The owner might deposit only 800 NYA at start, then add another 200 NYA along the way (between block 500 and 600). That is indeed flexible.
Unlike the UniswapRewards contract, which is very strict on these. Though it prevents mistakes and middle-man from rug-pulling. It’s strictness defined it’s safetyness and also it’s rigidness (on the good side). Most people simply avoid using this contract for yield farming because of it’s strictness and complication, but we at Nyanswop totally love it.
Both has their good and bad points. It really depends on what you’re looking for.
- If you’re sending with ‘self-burning’ tokens, make sure to calculate the starting amount correctly by taking into account of the end state. For you to send 21 BLIQ, with 6% burn rate, you have to send 21/0.94 BLIQ.
- If you’re dealing with tokens with ‘overriden’ methods (especially if it belongs to the official ERC20 interfaces), do check if the end state is what you expected. For our case, BLIQ token has a burning mechanism for the ‘transfer’ method and we didn’t expect to not receive 21 BLIQ.
- If you’re using contracts that are very strict on the data integrity, do take extra note when dealing with tokens with unique personalities.
- If a transaction fails, there’s no need to repeatedly send the same thing because it is bound to fail. Do something different, or reach out to the support team of the Dapp that you’re using (including us nya~ 🐱)
We like Uniswap’s RewardsFactory’s strictness as it maintains the integrity of the data. So we’ll not modify our contract to fit special tokens, at least for now.
After this interesting issue. We now require all partners to do a trial-run with us in the Testnet if your token has certain methods overridden (especially transfer method).
We thank Bliquid for being very responsive in reacting to our request, and also explaining in-depth on the burning mechanism of the BLIQ token.
— Nyan Dev 🐱
Follow us on Twitter and Telegram for the latest updates.