NEAR Live Contract Review | Episode 5: Berry Club
Nothing is loaded from YouTube until you press play, and then without tracking cookies.
About this video
This week we have a Special Edition 5th episode of our live contracts review series for the Berry Club. Get insights and explanations directly from NEAR Core team members as they talk through the intricacies of important contracts on NEAR.
Follow the latest from NEAR Protocol on:
Website: nearprotocol.com Discord: near.ai/discord Medium: near.ai/medium Twitter: near.ai/twitter GitHub: near.ai/github
#WhiteboardSeries #Blockchain #FutureIsNEAR
Transcript
6,360 wordsCaptions as YouTube publishes them, usually machine-generated, so product and people names can be misheard.
- 0:01
hey uh this Eugene and Mike Josh Chad and Sheriff we here together on another weekly episode of Life contracts and today is a Barry Club special exciting um so today we're going to talk about Barry Club contract and the interesting part about this is life upgrade that happened and how we actually updated an existing state by adding a banana so let me present you first the bar club itself so Berry Club is um a demo app that I build
- 0:53
to demonstrate what near protocol is allow us to do and so Barry Club allows you basically to draw pixels on the board and the board getting updated and as you can see I don't have avocado the way it works you when you join with the wallet and I'll join with my different account so so when you join a login with the wallet you get um access key to this front end application a limited
- 1:41
access key that allows you to call Barry Club contract and in this case the bar club contract has a public board where everyone can draw and you can later draw pixels on the board and the contract records them internally you can buy aada token with near and that allows you to draw more pixels for every pixel you draw you pay one avocado token and then there's a now farming preferences that allows you to farm either
- 2:35
avocados which allows you to draw more pixels or bananas which is a tangible token on this contract so I will not go into more details on how this works but it works in through RPC so RPC calls like the frontend calls RPC to get the latest board and refreshes it occasionally and it does this efficiently to not fetch data that it doesn't need um so let's go into the contract to explain the basic
- 3:27
details okay the contract is so this is the version of the contract before the upgrade and it only has avocado and you can only Farm avocados so when you draw something it generates avocado for every Pixel It generates one avocado a day the avoc is a fible token it's split into a Precision so you can have a partial avoc and the contract doesn't really care about the storage as much because you
- 4:09
cannot send avocado so you cannot really abuse the storage as easy you need multiple accounts so as usual we have a main structure it's called place has a map of account indices so slightly different from usual where we have account ID to some account structure instead it relies on given an index uh us6 us32 index for every account unique account so when someone else joins for the first time they got
- 4:56
they get a next index available then we have a board which stores pixels and ownership of pixels then we have list of account a vector of account this is also a persistent vector and when a new account joins for the first time and draw it first pixel this account is added to the end of this vector and account ID is added to list of indic then there's some extra data for statistics when this contract was first
- 5:36
deployed then um total amount of avocado purchased and total amount of avocado burn to draw this is the standard default and when it's first initialized it creates an account for it itself and assigns all pixels to itself there's um one method to buy AOC and the way it works um we grab the account by ID this either either returns an existing account or creates a new account um
- 6:33
then it calls internal method to buy tokens and Records how many tokens we bought saves the account and there is another method which is draw uh but to get into draw we first need to figure out how that data is stored for this we have pixel board which is initialized with pixel board new so this is just a internal structure it has a persistent Vector of lines so we store the pixel board
- 7:16
into um the following each line each row is stored as one record and also we have a metadata field called line versions which is incrementing when you modify a line so how is a line a row yes a row so um so when you want to update the board you first for the first time you fetch into board so you fetch all lines so you need to read 50 keys from the state because there's 50 by 5050 and you get all the lines but you
- 8:13
also get a record of uh line versions so this is a metadata for every line or every row that's saying somewhat like where what when what is the L last not like how many times this row was updated essentially now to refresh aort you don't refetch all 50 lines every time because that would be too much key to that you need to read instead you just fetch line versions so you get one key from the
- 8:56
state and line version for example say oh line or like row number one changed and row number three changed so you only need to fetch rows that are modified then you remember the new versions of the rows and next time you update and fetch next versions you only will need to change the rows that will updated again so that's how it does this efficiently um it would be possible to rearrange this slightly if locality is
- 9:38
more important so instead of relying on rows you can rely on somewhat like a cells so you can break the field into instead of 50 rows you can break it into like 5x five cells and maybe have it 100 cells this this way when you draw locally you only need to update four cells instead of maybe 10 rows uh pixel line is a vector of pixel pixel is a color which is um user2 so four bytes and owner ID so this is the owner ID or account ID from the indexes this is why the
- 10:29
um assigned a unique index for every account so you can more efficiently store for every pixel the owner of the given pixel um so by default the owner is zero which is this particular contract which owns all the pixels and the color is white um so now let's look at the account structure the account structure is the account ID account index so we don't really care because uh this data can be actually got from the keys of
- 11:14
corresponding uh fetch but it's easier just store it internally then the balance is U1 128 so this is number of Avoca pixels well not pixels number of Avoca tokens and the Precision of one avoc is something like 10 power of 18 and then finally we have a time stamp which um in Nan seconds that is when you last time touch the given account uh and we have number of pixels on the board for this account
- 12:01
so why do we need all this information it is because um when you have number of pixels balance and time stamp you can calculate how many rewards you acquired at the given moment so let's say you had five pixels and uh one day passed then we can say that you acquired uh five avocats for this and that allows us to update the account in a constant time instead of um like refreshing or giv the avas dynamically every time some moment passed to all
- 13:02
accounts so now what happens if you replace a pixel of someone else uh that means that the number of pixels of the owner of this pre previous owner of this pixel decreased by one because you draw on top of this account it means their rate at which they receive pixels has to change it decreases so what we do we essentially calculate at the given moment how many pixels or how many tokens the account should receive give them tokens and set the claim time stamp to the current time
- 13:56
stamp this way and we decrease the number of pixels so you updated the previous balance and changed the rate from now on they will earn like one pixel less and when you draw something you need to do the same we need to calculate how many pixel you had before so we do this uh it's called touch on this account and this is basically kind of an update account event so you take the current block Tim stamp we calculate the
- 14:29
time difference how much time in Nan seconds passed and then the update the balance using uh one extra avocad per day so for example if you don't have any avocados and don't have any pixels you can still Farm them using one avocado a day so when someone overwrites someone else's Pixel It updates both their account and the and that other person's account yes yes and that's why um we are limited in
- 15:05
how many pixels you can update per batch so if you don't instead of um instead of drawing only one pixel at a time you can draw multiple pixels and you'll get to there and so yeah so the way we update balance is basically we calculate how much balance we need to give then we use a constant which is reward per pixel per nond which is a Precision One pixel divided by number of NCS in a
- 15:51
day um so let's go to draw so Matt welcome to this call today discussing Barry Club so it's a very exclusive Club I feel honored okay so the way draw works it takes uh a vector of pixel request pixel request is a structure that has a coordinate x y and a color probably can be optimized to pass less data but it's not very important there's a validity check so it should be within boundaries and the color should be within boundaries so the way Draw Works we
- 16:53
first um get the account this things also has touch so if the existing account was there then it will first give all the tokens back to the account and set the current claim then we see how many new pixels you want to draw and we charge the account for this amount of pixels so we basically just cut it out of balance we say how many pixels were burned or like how many tokens were burnt for drawing and then
- 17:44
we get a board method called set pixels it's saying who is the new owner and the list of pixels and it returns us a hashmap is say um how many pixels were deleted for the previous owners and who were the owners so set pixels works the following way it uh goes through all requests it has a hashmap of updated lines and it uses some Rust um method to get an entry and and it basically gets a line updates the owner and updates the
- 18:35
color gets the old owner and um and it uh saying that the old owner key was deleted and then it saves the line I think it's possible to mess it up if you draw the same pixel twice and the owner maybe I'll fix it later I need to check um all I'm interested to hear more about what that bug might be I'm also curious why the U size which is maybe a rust like I know what rust I know what uze is
- 19:21
from rust but why use it in this usze is uh used for indices so so when you access an index from the vector you use U size or when you get the length of the vector nonpersistent vector it also see us size but because of wasm we want to be a more consistent and we don't use us in was uh arguments I'm not sure if you're talking about the this as a bug or not but I did notice that when I tried to overwrite my same pixel with the same color it didn't
- 19:55
charge me um avocado so that's actually awesome behavior and maybe that's expected from what you're showing us here so this is probably on a front end uh but the park would be if you overwrite actually it's not maybe it's not a bug but if you overwrite your own pixel multiple times here then it will say that you draw for example five pixels and then you deleted the owners five times which is fine you
- 20:33
already a new owner here um I think it works okay yeah it should seems to be working okay because you just charge yourself um multiple times it will here remove your own account and we say you added for example 10 pixels and erased six pixels of yourself yeah there's no bug but it's it's weird implementation um so basically yeah what what happens is we modify lines and we remember so each line that we modify you remember it in a temporary map a hash map so if one line was
- 21:28
modified five times we don't save it immediately instead we iterate on the lines at the end and we call Save line for row it basically replaces it in a vector and increments the vector of this line and finally we return the hash map of owners and how many pixels were erased for the every owner previous owner so then we go through all accounts except not yours because your account is updated here so you added for example five pixels and
- 22:11
replaced one of your own pixels so you got a total of four new pixels but you will be charged for five new pixels and we save your account and then we go through all previous owners and again using this method which is um which gets account by account index and decrease their number of pixels and then save their account back to the state so that's how the mass works on um on drawing and updating the balances
- 22:59
finally you can get a leaderboard because the front end has the pixel owners it's locally sort owners by the number of pixels they own so they don't really need this num account data and then one by one it queries all the owners to draw this leaderboard and get their rate so that was pre- banana release any questions on how that worked is an avocado a berry yes or no yes okay okay uh let's go into the branch a latest
- 23:57
branch and talk about how the migration happened as you can see um the previous structure held had a vector of a Legacy account version uh the upgrade needed to introduce new balance for bananas which required to modify account itself because there could be potentially more than 200 accounts um it's um it wouldn't be possible to migrate them in one transaction potentially because it may be too much data so instead we use a lazy upgrade so
- 24:59
whenever you touch an account the Legacy account be upgraded and ideally you want to do an upgrade in in in minimum amount of reads so probably want to like read a a new account first if it exists and if it doesn't exist then you fall back on the reading the Legacy account because if a new account exists in a good case scenario long-term scenario you only wasted one read or you spent
- 25:39
one read but in in upgrade scenario you will have to do two reads so you first read the like non-existing or like original and then you look up the Legacy oh you you read the latest account and then you read the try to read the Legacy count so the way it's done we introduce two new fields on the new structure uh number of accounts and a lookup map you cannot use the vector because Vector doesn't have a
- 26:23
skipped field so like you cannot just have some item that missing and set length to the larger one so we needed to switch the look up map for this we need to store a length of this data explicitly uh as a separate Shield then we rename the old account into account version ofad which is exactly the same structure this was before and we name rename the old structure to place version avac so this is like a first version while the new version called just Place
- 27:11
finally introduced uh more balances so there's a renamed mint amount renamed to B balances burn amount the burn balances and we added Farm balances to track how many actually buries were farmed uh and this is part of fible tokens also uh we introduced a new account structure called upgradable account to prevent mess of like introducing Legacy account and this in the future so upgradable account is enam which has a version of account so
- 27:59
the same field can store different versions and then you have a field that converts upgradable account into account that is a structure that we will use so this is the most recent account structure so all code will work with account and not upgradable account an upgradable account is only needed to read and write and then we have from account to upgradeable account which will put it at the latest version which is right now
- 28:38
Banana account so if you want to introduce let's say a new Barry all we need to do is introduce this version to to be something like cucumber account and then um implement a migration from banana account to to the latest account version and then saing is just fine so the new account version is uh modified balances so now the balances is a structure or it's a vector which has like two balances instead of one and introduce the farming preference which
- 29:28
you can pick what you want to farm okay going back to how the upgrade happened is we introduced a new Constructor called migrate from version avocad and it justed in it because we don't want to read the Old State but the Old State Should exist already and we want to write a new state so we do this with the following the first line is we read the avoc version of the state and we say State read is
- 30:28
actually borch der serializable object so it it will try to read the state of a given type in this case option of place aada and then we interrupt this into non option because we want the state to already exist so if you will call this method right now this will fail because it's um will have a mismatch on parsing the stage can I interject real quick in case people are watching this video and haven't run into this if you didn't have the init on top of here it would try to
- 31:11
read the state uh and match it with kind of like the structs that you have and you would get State mismatch and you would kind no I think if you don't have in it but have mute self then yes but if you don't have AER it kind of static method I think without in it it will not write the state back to this storage so you have to manually handle state right so you need to you need to to do two things you need it to not you you do need it to write and you
- 31:49
do need it to not try to read yes yeah yeah so manually read I think it's still an issue the way I did it because if for some reason it still deserializes then it might be a conflict uh but I don't think it does deserialize properly because it would fail on the vector here somewhere um still probably not the best way to do this uh and ideally you would protect against this by saying maybe you will attempt to read the previous state or something or like
- 32:34
write an extra field which is like what's the current version so you will try to read the current version if it's already exist and you don't do this um and then you just do a migration from previous version to the new version by put fields in a proper place so n accounts is like a lengths of previous accounts look lookup map is empty with the new field you introduce WS for token transfer migrated balances
- 33:06
properly and reuse the account vectors and then you have a new this is in case you want to deploy it freshly to the new version so this is a standard Constructor but now it's construct a second version uh okay so this is how we migrated uh uh the global State like the the singl ton state of the contract itself now we need to be able to migrate accounts so we have this um method something like um get insal account by ID it was previously just called get account by ID
- 33:59
so what it does it actually does the following so it tries to read the index from a new lookup map if it does exist which returns an upgradable account then it converts it to account to to regular account but if it doesn't exist in the new account then it will try to read it from a Legacy account field which is it will read the avoc version of account and convert it to an account
- 34:46
through migration so this part isop is how you convert from um from avac version to account so you just convert balances and introduce the default farming preferences Avada so that's how you read sorry go ahead that's how you read it so are you are you writing it are you moving it from one data structure to another right so so the moment you save it is when we're going to write it to the new place
- 35:26
so here's a save account um the first check if this is a new account oops the new account means account index is more than the current one then we just need to store it in a structure and also introduce save the new index and increase the number of accounts it's for sure not a Legacy account because it's more than current existing number and if it is not then we first try to store it in a new map and if it didn't exist so if
- 36:14
it's Legacy account that didn't exist in a new map it means it only existed in a legacy Vector but not in in the current version of a in this case we can also free Up Storage from the vector by making a vector inconsistent so this is how persistent Vector stores data um it has a prefix and prefix a and then it has of one by and then it has a length of u64 for the index which we write index ID so we Rec
- 37:04
recover it the row key that used um for how the vector stores data and then we just delete it we basically say uh let's delete it it will make the vector inconsistent in case we're going to need to read this value again from the vector the country going to panic but because we never do if it all works correctly uh this works that looks really scary all right so so the correct way of
- 37:43
deleting from Vector you have swap remove but this will modify the indices of uh accounts and just decid I decid not to do this and it seems uh possible to do at least for this case if you have to do another migration we will not have a problem because now we using a lookup map so a lookup map has a Del deltion because it can have um missing keys but Vector expects all keys to be present so correct me if I'm wrong but I think the beauty of this because I I've think I've tried to struggle with
- 38:29
like how you do upgradeability and like kind of ran ran into the thought that like you know you want to make sure that someone who's paying gas for a transaction is is probably only going to use their own gas on their own stuff and that they're not like donating gas to like help the upgradeability of the contract or maybe you do but maybe you want to have that be like in some sort of like terms of service almost um but
- 38:55
it looks like what you're doing here is like it's upgradeable but you're only ever upgrading yourself with your own gas not really because if you override someone else account then you're upgrading their account as well okay huh but you kind of do an action that touches D so somewhat you influence in this so that's pretty much it uh from the upgrade perspective uh the touch changed a little
- 39:31
because bananas are not farmed if you don't have any pixel so you only get extra avocado a day but not for extra banana and this is the difference when you change a farming preference from avocado to Banana you farm one pixel less um I haven't talked about how you buy tokens but especially with discounts there's a deal that gets you extra avocado if you buy Above five near and
- 40:08
the way it works it just checks if you pay it more than five near five or more then it gives you all avocado discounted price otherwise it gives you default price um and we can talk about how the token contract works and like that that introduced the token contract as well I introduce a new field as we discussed in a previous meeting about a VA fible token or like it was save B save based
- 40:48
token and we had suggestion to rename it to Vault um also transfer on Save is rename to transfer row because you basically just deposit on your receiver account and I implemented basically I just wrote a trait the comments has to be updated because they were not converted from saves to volts but apis were converted so you have a this trade that you can just copy and paste in your own contract if you want to implement um
- 41:34
a 122 for your own token and then you have impul trade name for place so I'm implementing this particular trade so this interface for for the contract so it has much nicer structure to it because now you only need to implement all fields that are required for this uh particular tra so let me just delete this to avoid to be cleaner so you have methods that you need to implement in order to support a given standard and if you implement them
- 42:20
correctly then um then it it's good so I basically copied most logic from the implementation that we had before I removed short hashes because we don't really worry about the um someone abusing it with a storage because it has a limitation that you cannot register an account for someone else so if you want to start using this token you first need to register account for yourself by calling register account it's not payable it's how like when you first
- 43:11
draw it registered an account for you but you can also just call a register account so I've got a couple I got a couple high level questions um you know but before we finish but I've been talking a lot I don't know if others have questions but this is super interesting and I'm glad to see an implementation of the funable token contract we saw last week does anyone else have thoughts um I I think I think one thing
- 43:46
that's really interesting is this is like a case study in terms of um sort of like profitability of uh smart contract and you know if you're not under strict it and want to mention like the gains that you've gotten from Barry Club I think that'd be really interesting and like because you have two things going on you have the 30% gas fee people people are using it like right now you know it's the most like one of the most
- 44:10
used apps on on mainnet and stuff so you have the 30% and then also you have this um buying the fungible tokens and whatnot and like it seems like from the outset like that people could actually like you're footing the bill for the state but you you are you're seeing positive returns correct right so so the total number I think if you look at the account I don't think I withdraw that much out I I moved my original investment for the storage
- 44:41
State and I think I did a few um maybe 10 year out for this something else so it got right now it's more than 500 so it's like 527 year that was used to to purchase avocadis and I don't plan to move them because I was working on the next uh version which allows you to actually use your bananas Farm bananas to stake them in order to receive the profit from this contract so it will
- 45:18
basically work similar to staking pool where you Stak bananas and then based on the total number number of banana staked the rewards are streaming from Barry Club contract to the next contract and distributed across the participants equally so for example if you own 10% of banana Stak then you will receive 10% of rewards and the way I want to do this is um so this bar club contract will every hour when someone draws
- 45:57
for the first time it will send a transfer which is uh 1% of liquid balance to to distribute rewards so it will keep some extra for the future state but also 1% will limit it from draining completely liquid balance because of Decay over time and it means in like in in the first day of farming it will probably distribute around 25% like 20% of the liquid balance which is around um 125 m to the owners of banana who will
- 46:48
stake so I will first introduce um Sten tool so people can actually pre- stake and only after this once like everyone aware that they can Farm or like they can use their bananas only after this introduce the rward stream and basically it closes the loop of where now uh you can draw something by buying avocados then you stake bananas like you farm bananas then you Stak bananas and you get near
- 47:29
back that you can use again to buy aads and because it's just a demo and I didn't really need it to profit on this uh I wanted mostly to test all the variable mechanism around like purchases then fible token and receiving the fible token on another contract is what also need to be tested so we currently only tested um just a raw transfer so I tried it on like using CLI and I was able to transfer for example two
- 48:15
bananas CLI and it worked so next will be to actually test this transfer Vault to make sure sure everything works and it's slightly simpler implementation because um you didn't need to burn tokens if account doesn't exist because you cannot delete the account on bur Club so you can only create new accounts and and just to be clear that when um that one part where you were actually deleting the state to save a little bit that was probably not
- 49:03
something that um average dap developers would need to do but you you did it because you used of of a certain kind of data structure instead of lookup map is that correct yes so because the vector well Vector allows you to delete a field but you cannot really change the index within the vector so for for for this one it was important that the index or the order within the vector
- 49:35
stays the same so and seems like vector doesn't expose this particular API that's why I had to do this manually because Vector doesn't want you to be in inconsistent state so if anything this is kind of an advertisement to use near collections well Vector is and collection so yes but you just need to make sure uh you think about upgradability a little earlier so a good tips on this is oh one one thing that I
- 50:10
didn't mention I introduced human accounts it's like human readable account while our account structure uses bche Fields this one actually flattens the balances into two fields and removes the claim time stamp because it's not needed because you always get up toate information and it creates a third serializable structure and you have just a converter that uh Maps count to a human account and then
- 50:48
you have a field that returns your entire structur and staking pools actually uses the same logic they have a field on them which has a full account information instead of only like manually or like doing this in multiple calls like get a balance get a un State balance and like like this so yeah so on upgradability if you think that you will need to upgrade an account I would recommend this type of
- 51:24
pattern so you implement some field which is inam upgradable account version one it can be uh the like a banana account structure but you can always rename it so it can be latest just account then you have a converter that converts from previous account to the next account and does this like in a loop and then and you have actual um converter back to the structure that just uses the correct in know so if I had a vakada account for example
- 52:13
here which would be the correct would be like this then here I would need to introduce um C version and I will get account and I'll just use account. into and that's how it will work so now I can upgrade from Avada version so let's say I need to introduce a new field so I will rename this to Banana account and this will be my new account so again I will just use inut here and then introduce a new field
- 53:00
uh which will be like this and here instead of banana I will return something like this so that's entire upgrade that you will need to do no more mapping from lookup map to a new one because adding a new inam to bch is not creates a conflict with all data it will work on all data so so that's super simple how you actually doing upgrades and maybe avoc will first need to be upgraded to Banana account which is fine but then banana account can be
- 53:46
upgraded to the latest account so you do kind of chain upgrade similarly how you migrate databases okay oh also about gas it's a shame that they didn't record how much near was spent on this particular contract explicitly so I cannot really tell how much near was received from the gas fees um versus how much near was uh received from bu explicitly it's possible to get from indexer
- 54:45
um but not uh not from the account itself not in storage and before before we close I want to also present Wayback machine which is uh was built by Vlad one second okay so basically uh because this contract was on chain uh you can query historic state from RPC nodes archival RPC nodes and this is what Wayback machine does it
- 55:51
basically if you click on someone so I clicked on Vlad here instead of going to explore now it goes to um the URL way back bar clubo which looks at the indexer and queries transaction of a given account and then renders States uh that was done by the given account so like if you do some transactions after some time it just uh selects a block uh height at which you did the transaction and then it queries this block height for a state of the
- 56:37
contract parses it and extracts this uh map so uh and basically it has some stuff but I think indexer is a limited to the number of return so it was not return in latest time and then you can just get it for all users which is just returns you kind the history of bar club drawing up to some point at which uh either indexer didn't reply something else so that's it on the contract in the bur Club um I
- 57:28
would also want to ask near developers so if someone Builds on new and wants their contract to be reviewed then um pin me anywhere on telegram or in like Dev Channel or Discord and maybe on the next week we will review your contract and record it and publish it home any questions is there going to be a holiday special on avocados I don't know it requires a contract change so it's not a super simple way of doing
- 58:16
some stuff take that you can change the price it does it's possible to change the price pretty easily if you just upgrade the contract but it would not be very fair for people who they like sweat farmed avocados and bananas in the past Fair well thank thanks everyone um hope you enjoyed it see you next time