NEAR Live Contract Review | Part 4: Berry Farm

(66 nL)
16 min read
To Share and +4 nLEARNs

Introduction

This is Eugene, and today we’re going to talk about the Berry Farm contract which is the supplement of Berry Club that allows you to convert your bananas to cucumbers, and farm NEAR. Let’s first do a quick review of the Berry Farm. The repository can be found on my Github. Here is the original video which this guide is based on:

UI

 

Berry farm is a very well designed app with large text, and lots of emojis. The way it works is that it displays your balances once you’re logged in. It pulls it from two different contracts as well as your account. It combines them from multiple sources. Avocado and bananas come from the Berry Club contract, cucumbers are local for that contract, and NEAR comes from your account balance. If you formed anything then you get rewards when rewards are distributed. It seems that someone made a lot of avocado purchases recently, and the rewards actually increased. It also has global stats which means 2 500 NEAR was transferred from Berry Club to Berry Farm. I earned 116.9 NEAR and my reward is going to be 0.014332 NEAR based on the amount of cucumbers I have compared to the total number of cucumbers. 

The total number of cucumbers is 54116.4. I get a 3% share of this reward every time there’s a reward. So when I click refresh I basically refresh stats from multiple accounts, and I can claim NEAR which it transfers from from the contract to my account. I can also swap bananas to cucumbers and I’m doing a call to the Berry Club contract.

I’m going to transfer_with_vault which is using the new standard, and it requires a small amount of NEAR, and I think it’s like 1 yocto NEAR to confirm that I’m not doing this through an access key.  I cannot make a transfer through an access key in oreder to avoid automatic withdrawing, and this is just a safety guard. If you would, for example, authorize Berry Club from some other app that tries to drain all of your bananas it would fail, because it will not be able to call transfer_raw or transfer_with_vault. To clarify, transfer_raw is the new name for transferring without a contract so you can just deposit with someone’s account that exists, and in transfer_with_vault vault is calling the receiver to withdraw tokens. 

I spent 10 bananas to get more cucumbers, and I can refresh the balance again to see if someone withdrew something. That’s how the UI works, but there’s more functionality to this than just this. The contract supports cucumbers as a fungible token, also bananas. You can potentially build another app that transfers cucumbers and there’s one more which is Banana Swap that Vlad Garchina built.

It is a simplified uniswap with a single pool where you can buy and sell bananas. I actually haven’t tried it before. I want to get 1 NEAR worth of bananas which I need to sell for 6 bananas. Let’s see if it works.

It calls Berry Club transfers which is once again involved, but to a different contract. We currently don’t display arguments, but eventually we will.

It’s saying I transferred, and I got 1 NEAR which I spent 6 bananas on. Pretty cool.

You cannot add to liquidity and it takes a 10% cut. So this is the UI.

Contract Review

Berry Club

Let me present the contract. So we’ll start with the Banana Farm contract, but you will probably need to take a look at the token contract from pixelboard, the contract from Berry Club, as well as the one that sends tokens. 

Berry club has a hard-coded contract called farm contract id.

When you draw, it verifies that you don’t draw empty pixels otherwise you can just trigger a reward without spending avocado. 

Then it calls maybe_send_reward which gets the current time from the blockchain, and checks that the next_reward_timestamp is less than the current time.

The next reward is either the beginning of farming time which was a global timestamp that originally enabled a 26 hour countdown or last_reward_timestamp when we last triggered it plus 60 seconds. 

So if it’s time to send the next reward it remembers the current time, and it calculates the reward based on this logic so it takes the current balance of Berry Club, it takes the current storage, computes storage by the storage cost plus some safety bar safety margin, which is 50 NEAR. 

It always keeps 15 NEAR for the future balances. Then it’s saying if balance is available, if our balance is higher than the amount we need for storage plus safety, then we calculate liquid balance, and we divide it by the portion of reward that we will give every time which is 24 times 68. If you would call it every minute then you will drain the majority of the account, but it is always kind of decreasing on itself, because the balance decreases on itself, but it mostly gives all balance within a day if you would call it every minute. First of all, you cannot produce a block that is timestamped earlier, and then you cannot produce a block beyond your slot, you have a dedicated slot when you need to produce a chunk. So the most you can do is produce a chunk of the final block at the final millisecond of your slot, which is one second ahead. You cannot really move it way forward otherwise all the other nodes will break the protocol as well, so they are limited by their understanding of the time plus the bar that they use to decide whether to accept a block or not, so you can’t really make a time run faster unless all validators want to do this. This would mean we have the validators which will essentially stop following the protocol. They need to coordinate their time. 

So at the end it got the reward it’s saying I’m going to distribute this reward, and what it does is it calls the farm contract, and calls take_my_near and it just doesn’t pass any arguments and passes its reward and gas. Just a minimal amount of gas needed to add the balances. That’s how Berry Club was updated to start distributing rewards after every drawing potentially. It takes slightly more. It uses gas that was spent on drawing to actually distribute the reward occasionally which needs about 10 tera gas for this.

Berry Farm

Let’s go to Berry Farm and start with this contract.  The way that berry farm works it needs to, in constant time, be able to distribute rewards based on the total number of cucumbers which is not a trivial task, it’s somewhat similar to the staking pool, and how the staking pool distributes rewards. The staking pool creates a bunch of shares for every deposit you make and the share has a current global price that’s saying, “Hey I purchased or minted more shares, and now my shares can be redeemed towards a given number of NEAR tokens.” The share price is starting from 1 yocto NEAR per share, and then the more rewards you accumulate the more the price of shares goes up. When you use unstake, you sell shares at the current price, and you get NEAR back, and it’s unstaked, and it  rounds down. The challenge there is when you stake cucumbers the price of a cucumber is zero, and when you first stake them  you cannot really buy shares and mint shares, because the price of them is zero. The logic of shares doesn’t work very well for this so for this thing I needed to create something different to do the accounting. Let’s go into examples. Let’s say Mike is the first person who deposited 10 cucumbers to the contract, and then let’s say 100 NEAR was distributed. Mike will take all the rewards because he is the only one, he controls 100% of the share on the Berry Farm now. Mike was offline for example, and didn’t claim them, so we cannot really update his account, because it has to be done in constant time. We cannot iterate through all of the accounts, but we somehow need to remember that Mike got all NEAR for his cucumbers. Now let’s say Josh comes in and stakes another 10 cucumbers now Mike’s share is 50% and Josh’s share is 50%, but Josh staked the cucumbers after this initial 100 NEAR was distributed already. He is not entitled to any of this now another 100 NEAR drops Mike now needs to get 50 more, because he has 50% stake and Josh gets 50 NEAR. When Mike comes back online we need to be able to display his balance at 150 NEAR that he earned in constant time. So this is the challenge that we needed to address with this contract, and the way it was solved is by having a global ratio called near per cucumber which is saying how much NEAR is received by a given cucumber balance when an account is created, or touched in any way. We remember the last value of this fraction. 

Then we have a NEAR balance, so if the last fraction was lower than the current fraction, it means we can multiply cucumbers of the given account by the difference between 2 fractions, and calculate how much NEAR I’m entitled to. When mike’s account was created the fraction was 0 divided by one and when 100 NEAR was deposited where the total cucumber count was 10, this fraction became equal to 10. Then once Josh joined and dropped another 10 cucumbers to the total, and another 100 NEAR were distributed, this fraction was increased by 5, because there was a total of 20 cucumbers and 100 NEAR. So 100 NEAR divided by 20 it means 5 NEAR per cucumber so this became 15. Now when Mike comes back he looks at his cucumber balance which is 10 near per cucumber on a global contract is 15 his last remembered value is zero so he just takes the difference which is 15 multiplied by 10 and claims this balance into NEAR, and this 1 is set back to 15. When Josh comes back, he gets this value which was at 10, and he has 10 cucumbers and the current value is 15. The difference is 5 multiplied by the cucumber balance of 10 so he gets 50 NEAR. That’s how we manage the NEAR balance, and appropriate the rewards received from people using constant time to recalculate it. So instead of keeping an entire fraction in memory so we use a global denominator which is fixed and I used it to 10 to the power of 18 which is the same as the precision of cucumbers, bananas and avocados. That’s the background of  how it’s all computed.

Let’s look at the main structure of the contract, a contract called farm. It has a lookup map of accounts, and it uses short hashes similar to fungible tokens with vaults. It has the banana token account id which can be hardcoded as well, but it was used from the contract because we can actually pass it, because this account was deployed after and I didn’t want to upgrade the banana Berry Club contract state. I didn’t want to modify the state again through upgradeability so instead I hardcoded the value in the previous contract. Near per cucumber, the fraction that I explained earlier, is the numerator and total cucumber balance is needed as a denominator. Vaults and next vote are used for transfers; they actually probably never ended up getting used to transfer cucumbers except maybe once for a test. 

This is what we saw before this is once again a hash of the account id.

The account structure is the last value before the account was touched with this fraction. The near_balance is the unclaimed balance. Whenever you touch an account either by depositing more cucumbers, or claiming NEAR it updates last value and moves all of the unclaimed NEAR to your local balance out of the difference, and cucumber balance is your balance in cucumbers. The near_claimed is the statistics, that is just saying how much reward you already claimed. It’s not needed for contract logic. 

There’s two helper structures just for the front end. The HumanAccount returns values in a more readable way and HumanStats is global statistics for consumption. Let’s go into the take_my_near function to actually take a look at how it works.

The first thing we do is check that there’s enough cucumbers there, because we use it as a denominator. We just want 1 cucumber to actually trigger this, and this is just a precaution. You don’t like to trigger this type of stuff. Let’s explore this value. What happens is you get the amount of NEAR, and yocto NEAR, and you multiply it by the denominator global. So you’re solving a fraction which is NEAR per cucumber divided by the denominator plus attached balance divided by total cucumber balance. Here is the formula: near_per_cucumber / denom + attached_deposit / total_cucumber_balance. So new_near_per_cucumber is actually the numerator with attached_deposit and the denominator then divided by the denominator. Here is the formula: new_near_per_cucumber = (near_per-cucumber_numer +denom + attached deposit) / denom. This is the formula we use to calculate the new_near_per_cucumber. Whenever you touch an account it always makes your current unclaimed balance into a fixed number, and then you can modify any of the values without breaking the logic. This touch method is called by every getter and every change method. Before you modify the account, it always calls touch on the account.

For example, in get_near_balance we get the internal account. If the account exists then we touch it locally. Then we should get the near_balance

If you get the account we touch it before then get your near_balance, cucumber_balance and claim balance. Those are view functions that are changing things, but it doesn’t save it so it just temporarily changes the account value, and doesn’t save it back. It just makes a touch which is a kind of  recompute with the latest value. 

It saves it when you actually do any change call, so for example call to claim_near. We get your account and get_mut_account actually touches the account. You get the account that is already updated, and near_balance is the actual balance. It takes the near_balance to zero, it’s saying you claimed all of this, and it saves your account. Then if you claim positive value then we will transfer it to you out of your account. It returns how much NEAR you claimed. The beauty of this touch method is that it can be called at any point. It doesn’t have to be called at the given point, so it is always gonna give you the same result no matter whether you called it twice in the middle or called it once. This is just for returning to the user like how much they’ve got. It just tells you how much NEAR you earned by calculating from the current state. In Berry Club we do the same stuff we have touch as well, and this is needed, because let’s say you want to know how many bananas you farmed. Since we know the last time you touched your account, when you last drew something, we know how many pixels you have. We can calculate from a given time to the previous time multiplied by the number of pixels you had how many bananas you should have right now, and the front end just does the same thing. The front end simulates it, it gets the same info except the latest, and then it just creates a timer going forward that calculates it. In theory the get_near_balance method doesn’t have to return your NEAR value that is up to date it, can just say here’s the total NEAR per cucumber and your last total NEAR per cucumber and also your last NEAR balance, but instead it’s done on the contract level. Let’s say we didn’t have view methods for which you don’t pay and only return and retrieve a state, then you would have to do this logic on the front end instead of doing it here in memory.

Token.rs

The final logic that we need to do is actually how you receive your cucumbers from Berry Club, and this is done by using transfer_with_vault from Berry Club. We previously discussed how map 122 works. 

You pass the receiver_id, amount,  and payload. It has an assert_paid check which just checks that the deposit is positive. It’s saying oh you need at least 1 yocto NEAR to prevent function access key calls to avoid a malicious Berry Club UI draining all your bananas. It gets the gas and then it initiates a call that  puts bananas into a vault. It also initiates a call to the receiver. In this case we call it an on-farm contract and it passes the sender id  an amount of bananas, a vault id which is just a u64 without JSON, and a payload which is a string. In the previous example, when we were talking about map 122 the payload was a binary of vec q8 in base64, because it was a borsh serialized payload. During the discussion I noted that if you would review this argument in a wallet you will not be able to see the payload that you want to do, so in this contract implementation we actually use JSON for the payload so that we insert the JSON from the string payload.  It’s an enum with a single option called deposit_and_stake.

Now when you do a function call from the farm UI that will go to the wallet in the arguments the payload will be like an adjacent string within a string saying deposit and stake instead of being some zero encoded as a base, instead of being a borsh serialized version, it’s a human readable one. This contract has different payload types than Uniswap, for example, it will have a payload saying how much NEAR you want to receive at most or receive at least. The malicious Uniswap UI can just actually replace this value, and if you didn’t verify it on the wallet side then you may actually get dropped under the front trend.  We got the account using get_mut_account which gets either an existing account,  or creates a new account where it sets all balances to 0, but the last near per cucumber numerator to be the current global one. This means you didn’t farm anything and you have zero cucumber balance. It does touch anyway even if it doesn’t need to, we could have avoided it if we did map,  and then it returns the hash and account. The account is already up to date. What we do is we first increase the total cucumber balance and then we save it back to the state of the account and we also increase the global cucumber balance by the amount of cucumbers deposited. Then we don’t really verify that the vault contained cucumbers so we trust that the previous caller, which is a banana contract, actually put the bananas into the vault, and we return the promise out of this method. It’s a weird match with a single entry because payload can be only one type right now. We call withdraw_from_vault, we state the amount we want to withdraw which is the full amount, and then we call it back to the banana token account id which is a Berry Club contract. Even so, this contract didn’t receive bananas yet, it doesn’t care because it doesn’t use bananas anyway. It doesn’t need bananas before it can proceed, so no matter how long it’s going to stake cucumbers on there, and the vault is already locked. It increases the total cucumber balance by diluting the shares. Now when take_my_near arrives it will use the new total cucumber balance. Now I will split the number of shares across all participants. 

Conclusion

That’s the overview of the Berry Club, Berry Farm and token.rs contracts. Thank you for reading and good luck in your future endeavors with NEAR.

Generate comment with AI 2 nL
773

1 thought on “NEAR Live Contract Review | Part 4: Berry Farm”

Leave a Comment


To leave a comment you should to:


Scroll to Top
Report a bug👀