diff --git a/docs/developers/tutorials/staking-contract.md b/docs/developers/tutorials/staking-contract.md index a20ea9193..813449f63 100644 --- a/docs/developers/tutorials/staking-contract.md +++ b/docs/developers/tutorials/staking-contract.md @@ -1,6 +1,6 @@ --- id: staking-contract -title: Staking smart contract tutorial +title: Staking smart contract --- [comment]: # (mx-abstract) @@ -9,31 +9,26 @@ title: Staking smart contract tutorial This tutorial aims to teach you how to write a simple staking contract, and to illustrate and correct the common pitfalls new smart contract developers might fall into. -If you find anything not answered here, feel free to ask further questions on the MultiversX Developers Telegram channel: https://t.me/MultiversXDevelopers +:::tip +If you find anything not answered here, feel free to ask further questions on the MultiversX Developers Telegram channel: [https://t.me/MultiversXDevelopers](https://t.me/MultiversXDevelopers) +::: [comment]: # (mx-context-auto) ## Prerequisites -[comment]: # (mx-context-auto) - -### mxpy +:::important +Before starting this tutorial, make sure you have the following: -We're going to use [**mxpy**](/sdk-and-tools/sdk-py/mxpy-cli) for interacting with our contracts. Follow the installation guide [here](/sdk-and-tools/sdk-py/installing-mxpy) - make sure to use the latest version available. - -[comment]: # (mx-context-auto) - -### Rust - -Install **Rust** and [**sc-meta**](/developers/meta/sc-meta) as depicted [here](/sdk-and-tools/troubleshooting/rust-setup). - -[comment]: # (mx-context-auto) +- `stable` **Rust** version `≥ 1.83.0` (install via [rustup](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) +- `sc-meta` (install [multiversx-sc-meta](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) -### VSCode +::: For contract developers, we generally recommend [**VSCode**](https://code.visualstudio.com) with the following extensions: - - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - - [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) + +- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) +- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) [comment]: # (mx-context-auto) @@ -41,7 +36,7 @@ For contract developers, we generally recommend [**VSCode**](https://code.visual Run the following command in the folder in which you want your smart contract to be created: -``` +```bash sc-meta new --name staking-contract --template empty ``` @@ -51,64 +46,99 @@ Open VSCode, select **File > Open Folder**, and open the newly created `staking- ## Building the contract -In the VSCode terminal, run the following command to build the contract: +In the terminal, run the following command to build the contract: ```bash sc-meta all build ``` After the building has completed, our folder should look like this: -![img](/developers/staking-contract-tutorial-img/folder_structure_2.png) -A new folder, called `output` was created, which contains the compiled contract code. More on this is used later. For now, let's continue. +```bash +├── Cargo.lock +├── Cargo.toml +├── meta +│ ├── Cargo.toml +│ └── src +├── multiversx.json +├── output +│ ├── staking-contract.abi.json +│ ├── staking-contract.imports.json +│ ├── staking-contract.mxsc.json +│ └── staking-contract.wasm +├── scenarios +│ └── staking_contract.scen.json +├── src +│ └── staking_contract.rs +├── target +│ ├── CACHEDIR.TAG +│ ├── debug +│ ├── release +│ ├── tmp +│ └── wasm32-unknown-unknown +├── tests +│ ├── staking_contract_scenario_go_test.rs +│ └── staking_contract_scenario_rs_test.rs +└── wasm + ├── Cargo.lock + ├── Cargo.toml + └── src +``` + +A new folder, called `output` was created, which contains the compiled contract code. More on this is used later. [comment]: # (mx-context-auto) -## Your first lines of Rust +## First lines of Rust -Currently, we just have an empty contract. Not very useful, is it? So let's add some simple code for it. Since this is a staking contract, we'd expect to have a `stake` function, right? +Currently, we just have an empty contract. Not very useful, is it? So let's add some simple code for it. Since this is a staking contract, we would expect to have a `stake` function, right? -First, remove all the code in the `./src/staking_contract.rs` file and replace it with this: +First, remove all the code in the `staking-contract/src/staking_contract.rs` file and replace it with this: ```rust #![no_std] -multiversx_sc::imports!(); +use multiversx_sc::imports::*; #[multiversx_sc::contract] pub trait StakingContract { #[init] fn init(&self) {} + #[upgrade] + fn upgrade(&self) {} + #[payable("EGLD")] #[endpoint] fn stake(&self) {} } - ``` -Since we want this function to be callable by users, we have to annotate it with `#[endpoint]`. Also, since we want to be able to receive a payment, we mark it also as `#[payable("EGLD)]`. For now, we'll use EGLD as our staking token. +Since we want this function to be callable by users, we have to annotate it with `#[endpoint]`. Also, since we want to be able to [receive a payment](/docs/developers/developer-reference/sc-payments.md#receiving-payments), we mark it also as `#[payable("EGLD)]`. For now, we'll use EGLD as our staking token. -:::note -The contract does NOT need to be payable for it to receive payments on endpoint calls. The payable flag at contract level is only for receiving payments without endpoint invocation. +:::important +The contract **does NOT** need to be payable for it to receive payments on endpoint calls. The payable flag at contract level is only for receiving payments without endpoint invocation. ::: -Now, it's time to add an implementation for the function. We need to see how much a user paid, and save their staking information in storage. We end up with this code: +We need to see how much a user paid, and save their staking information in storage. ```rust #![no_std] -multiversx_sc::imports!(); +use multiversx_sc::imports::*; #[multiversx_sc::contract] pub trait StakingContract { #[init] fn init(&self) {} + #[upgrade] + fn upgrade(&self) {} + #[payable("EGLD")] #[endpoint] fn stake(&self) { - let payment_amount = self.call_value().egld_value().clone_value(); + let payment_amount = self.call_value().egld().clone(); require!(payment_amount > 0, "Must pay more than 0"); let caller = self.blockchain().get_caller(); @@ -126,39 +156,42 @@ pub trait StakingContract { } ``` -`require!` is a macro that is a shortcut for `if !condition { signal_error(msg) }`. Signalling an error will terminate the execution and revert any changes made to the internal state, including token transfers from and to the SC. In this case, there is no reason to continue if the user did not pay anything. +[`require!`](/docs/developers/developer-reference/sc-messages.md#require) is a macro that is a shortcut for `if !condition { signal_error(msg) }`. Signalling an error will terminate the execution and revert any changes made to the internal state, including token transfers from and to the smart contract. In this case, there is no reason to continue if the user did not pay anything. -We've also added `#[view]` annotation for the storage mappers, so we can later perform queries on those storage entries. You can read more about annotations [here](/developers/developer-reference/sc-annotations/). +We've also added [`#[view]`](/docs/developers/developer-reference/sc-annotations.md#endpoint-and-view) annotation for the storage mappers, allowing us to later perform queries on those storage entries. You can read more about annotations [here](/developers/developer-reference/sc-annotations/). -Also, if you're confused about some of the functions used or the storage mappers, you can read more here: +If you're confused about some of the functions used or the storage mappers, you can read more here: -- [https://docs.multiversx.com/developers/developer-reference/sc-api-functions](/developers/developer-reference/sc-api-functions) -- [https://docs.multiversx.com/developers/developer-reference/storage-mappers](/developers/developer-reference/storage-mappers) +- [Smart Contract API Functions](/developers/developer-reference/sc-api-functions) +- [Storage Mappers](/developers/developer-reference/storage-mappers) -Now, I've intentionally written some bad code here. Can you spot the improvements we can make? +Now, there is intentionally written some bad code here. Can you spot any improvements we could make? -Firstly, the last _clone_ is not needed. If you clone variables all the time, then you need to take some time to read the Rust ownership chapter of the Rust book: [https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) and also about the implications of cloning types from the Rust framework: [https://docs.multiversx.com/developers/best-practices/biguint-operations](/developers/best-practices/biguint-operations). +1. The last `clone()` from `stake()` function is unnecessary. If you're cloning variables all the time,s then you need to take some time to read the [ownership](https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) chapter of the Rust book and also about the [implications of cloning](/developers/best-practices/biguint-operations) types from the Rust framework. -Secondly, the `staking_position` does not need an owned value of the `addr` argument. We can take a reference instead. +2. The `staking_position` does not need an owned value of the `addr` argument. We can pass a reference instead. -And lastly, there's a logic error. What happens if a user stakes twice? That's right, their position will be overwritten with the newest value. So instead, we need to add the newest stake amount over their current amount, using the `update` method. +3. There is a logic error: what happens if an user stakes twice? Their position will be overwritten with the new value. Instead, we should add the new stake amount to their existing amount, using the [`update`](/docs/developers/developer-reference/storage-mappers.md#update) method. -After fixing the above problems, we end up with the following code: +After fixing these issues, we end up with the following code: ```rust #![no_std] -multiversx_sc::imports!(); +use multiversx_sc::imports::*; #[multiversx_sc::contract] pub trait StakingContract { #[init] fn init(&self) {} + #[upgrade] + fn upgrade(&self) {} + #[payable("EGLD")] #[endpoint] fn stake(&self) { - let payment_amount = self.call_value().egld_value().clone_value(); + let payment_amount = self.call_value().egld().clone(); require!(payment_amount > 0, "Must pay more than 0"); let caller = self.blockchain().get_caller(); @@ -179,117 +212,142 @@ pub trait StakingContract { [comment]: # (mx-context-auto) -### What's with the empty init function? +### What's with the empty init and upgrade function? -Every smart contract needs to have a function annotated with `#[init]`. This function is called on deploy and upgrade. For now, we need no logic inside it, but we still need to have this function. +Every smart contract must include a function annotated with [`#[init]`](/docs/developers/developer-reference/sc-annotations.md#init) and another with `#[upgrade]`. + +The `init()` function is called when the contract is first deployed, while `upgrade()` is triggered during an upgrade. For now, we need no logic inside it, but we still need to have those functions. [comment]: # (mx-context-auto) -### Creating a devnet wallet +## Creating a wallet :::note -You can skip this section if you already have a devnet wallet setup. +You can skip this section if you already have a Devnet wallet setup. ::: -Let's create a devnet wallet. Access the [Web Wallet](https://devnet-wallet.multiversx.com/), and select "create new wallet". Save your 24 words (in the given order!), and create a password for your keystore file. - -Now, we could use the keystore file with a password, but it's more convenient to use a PEM file. To generate the PEM file from your secret phrase, follow [these instructions](/sdk-and-tools/sdk-py/mxpy-cli): +Open the terminal and run the following commands: -TL;DR: open the terminal and run the following command. Write your secret phrase words in order: - -``` +```sh mkdir -p ~/MyTestWallets -mxpy wallet convert --in-format=raw-mnemonic --out-format=pem --outfile=~/MyTestWallets/tutorialKey.pem +sc-meta wallet new --format pem --outfile ~/MyTestWallets/tutorialKey.pem ``` -:::note -You have to press "space" between the words, not "enter"! -::: - [comment]: # (mx-context-auto) -### Deploying the contract on devnet +## Deploy the contract -Now that we've created a wallet, it's time to deploy our contract. **Make sure you build the contract before deploying it**. Open a command line and run the following command: +Now that we've created a wallet, it's time to deploy our contract. -```bash +:::important +Make sure you build the contract before deploying it. Open the terminal and run the following command from the contract root directory: - mxpy --verbose contract deploy --bytecode=~/Projects/tutorials/staking-contract/output/staking-contract.wasm \ - --recall-nonce --pem=~/Downloads/tutorialKey.pem \ - --gas-limit=10000000 \ - --send --outfile="deploy-devnet.interaction.json" --wait-result \ - --proxy=https://devnet-gateway.multiversx.com --chain=D +```bash +sc-meta all build ``` -:::note -If you wanted to use testnet, the proxy would be `https://testnet-gateway.multiversx.com` and the chain ID would be `"T"`. For mainnet, it would be `https://gateway.multiversx.com` and chain ID `"1"`. - -More details can be found [here](/developers/constants/). ::: -The things you need to edit are the CLI parameters `--pem` and `--bytecode` with your local paths. +Once the contract is built, generate the interactor: -[comment]: # (mx-context-auto) +```bash +sc-meta all snippets +``` -### Account was not found? But I just created the wallet! +Add the interactor to your project. In `staking-contract/Cargo.toml`, add `interactor` as a member of the workspace: -You're going to see an error like the following: +```toml +[workspace] +members = [ + ".", + "meta", + "interactor" # <- new member added +] +``` + +Next, update the wallet path for sending transactions. In `staking-contract/interactor/src/interact.rs` locate the function `new(config: Config)` and **modify** the `wallet_address` variable with the [absolute path](https://www.redhat.com/en/blog/linux-path-absolute-relative) to your wallet: + +```rust +let wallet_address = interactor + .register_wallet( + Wallet::from_pem_file("/MyTestWallets/tutorialKey.pem").expect( + "Unable to load wallet. Please ensure the file exists.", + ), + ) + .await; +``` + +Finally, deploy the contract to Devnet: ```bash -CRITICAL:cli:Proxy request error for url [https://devnet-gateway.multiversx.com/transaction/send]: {'data': None, 'error': 'transaction generation failed: account not found for address erd1... and shard 1, err: account was not found', 'code': 'internal_issue'} +cd interactor/ +cargo run deploy ``` -This is because your account has no EGLD in it, so as far as the blockchain is concerned, the account does not exist, as it has no transactions from or to it. +:::note +To use Testnet instead, update `gateway_uri` in `staking-contract/interactor/config.toml` to: `https://testnet-gateway.multiversx.com`. -But still, how come you're seeing the contract's address if the deployment failed? +For mainnet, use: `https://gateway.multiversx.com`. + +More details can be found [here](/developers/constants/). +::: + +You're going to see an error like the following: ```bash -INFO:cli.contracts:Contract address: erd1qqqqqqqqqqqqq... -INFO:utils:View this contract address in the MultiversX Devnet Explorer: https://devnet-explorer.multiversx.com/accounts/erd1qqqqqqqqqqqqq... +error sending tx (possible API failure): transaction generation failed: insufficient funds for address erd1... +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace ``` -This is because contract addresses are calculated from the deployer's address and their current account nonce. They are not random. So mxpy calculates the address beforehand and displays it in the terminal. Additionally, the deployed contract is always in the same shard as the deployer. +This is because your account has no EGLD in it. [comment]: # (mx-context-auto) -### Getting EGLD on devnet +### Getting EGLD on Devnet -There are many ways of getting EGLD on devnet: +There are many ways of getting EGLD on Devnet: -- through the devnet wallet -- through an external faucet -- through the [MultiversX Builders Discord Server faucet](https://discord.gg/multiversxbuilders) -- asking a team member on [Telegram](https://t.me/MultiversXDevelopers) +- Through the Devnet wallet; +- Using an external faucet; +- From the [MultiversX Builders Discord Server faucet](https://discord.gg/multiversxbuilders); +- By asking a team member on [Telegram](https://t.me/MultiversXDevelopers). [comment]: # (mx-context-auto) #### Getting EGLD through devnet wallet -Go to https://devnet-wallet.multiversx.com and login to your devnet account with your PEM file. On the left side menu, select the "faucet" option: +Go to [Devnet Wallet](https://devnet-wallet.multiversx.com) and login to your account using your PEM file. From the left side menu, select *Faucet*: + ![img](/developers/staking-contract-tutorial-img/wallet_faucet.png) -Request the tokens. After a couple seconds, refresh the page, and you should have 30 xEGLD in your wallet. +Request the tokens. After a few seconds you should have **5 xEGLD** in your wallet. [comment]: # (mx-context-auto) #### Getting EGLD through external faucet -Go to https://r3d4.fr/faucet and submit a request: +Go to [https://r3d4.fr/faucet](https://r3d4.fr/faucet) and submit a request: ![img](/developers/staking-contract-tutorial-img/external_faucet.png) -Make sure you selected "devnet" and input your address! It might take a bit depending on how "busy" the faucet is. +Make sure you selected `Devnet` and input **your** address! + +It might take a little while, depending on how "busy" the faucet is. [comment]: # (mx-context-auto) ### Deploying the contract, second try -Now that the blockchain knows about our account, it's time to try the deploy again. Run the `deploy` command again and let's see the results. Make sure you save the contract address. mxpy will print it in the console for you: +Run the `deploy` command again and let's see the results: ```bash -INFO:cli.contracts:Contract address: erd1qqqqqqqqqqqqq... +sender's recalled nonce: 0 +-- tx nonce: 0 +sc deploy tx hash: 8a007... +deploy address: erd1qqqqqqqqqqqqq... +new address: erd1qqqqqqqqqqqqq... ``` -Alternatively, you can check the address in the logs tab in explorer, namely the `SCDeploy` event. +Alternatively, you can check the address in the logs tab on [Devnet Explorer](https://devnet-explorer.multiversx.com/transactions), namely the `SCDeploy` method. [comment]: # (mx-context-auto) @@ -298,163 +356,121 @@ Alternatively, you can check the address in the logs tab in explorer, namely the Everything should work just fine, but you'll see this message: ![img](/developers/staking-contract-tutorial-img/too_much_gas.png) -This is NOT an error. This simply means you provided way more gas than needed, so all the gas was consumed instead of the leftover being returned to you. This is done to protect the network against certain attacks. For instance, one could always provide the max gas limit and only use very little, decreasing the network's throughput significantly. +This is **not** an error. This simply means you provided way more gas than needed, so all the gas was consumed instead of the leftover being returned to you. + +This is done to protect the network against certain attacks. For instance, one could always provide the maximum gas limit and only use very little, decreasing the network's throughput significantly. [comment]: # (mx-context-auto) ## The first stake -Let's call the stake function: +Let's update the `stake` function from `staking-contract/interactor/src/interact.rs` to do the first stake. -```bash -mxpy --verbose contract call ${SC_ADDRESS}... \ - --proxy=https://devnet-gateway.multiversx.com --chain=D \ - --send --recall-nonce --pem=~/MyTestWallets/tutorialKey.pem \ - --gas-limit=10000000 \ - --value=1 \ - --function="stake" +Initialize the `egld_amount` variable with `1` instead of `0`: + +```rust +let egld_amount = BigUint::::from(1u128); ``` -To pay EGLD, the `--value` argument is used, and, as you can guess, the `--function` argument is used to select which endpoint we want to call. Make sure to adjust the first argument of the contract call command, with respect to the address of your previously deployed contract. +This variable represents the amount of EGLD to be sent with the transaction. -We've now successfully staked 1 EGLD... or have we? If we look at the transaction, that's not quite the case: -![img](/developers/staking-contract-tutorial-img/first_stake.png) +Let's stake! At path `staking-contract/interactor` run the following command: -[comment]: # (mx-context-auto) +```bash +cargo run stake +``` -### I sent 1 EGLD to the SC, but instead 0.000000000000000001 EGLD got sent? +We've now successfully staked 1 EGLD... or have we? -This is because EGLD has 18 decimals. So to send 1 EGLD, you actually have to send a value equal to 1000000000000000000 (i.e. 1 \* 10^18). The blockchain only works with unsigned numbers. Floating point numbers are not allowed. The only reason the explorer displays the balances with a floating point is because it's much more user-friendly to tell someone they have 1 EGLD instead of 1000000000000000000 EGLD, but internally, only the integer value is used. +If we look at the transaction, that's not quite the case: + +![img](/developers/staking-contract-tutorial-img/first_stake.png) [comment]: # (mx-context-auto) -### But how do I send 0.5 EGLD to the SC? +### Why was a smaller amount of EGLD sent? -Since we know EGLD has 18 decimals, we have to simply multiply 0.5 by 10^18, which yields 500000000000000000. +This happens because EGLD uses **18 decimals**. So, to send 1 EGLD, you actually need to send the value `1000000000000000000` (i.e. 1018). -[comment]: # (mx-context-auto) +The blockchain works only with unsigned integers. Floating point numbers are not allowed. The only reason the explorer displays the balances with a floating point is because it's much more user-friendly to tell someone they have 1 EGLD instead of 1000000000000000000 EGLD, but internally, only the integer value is used. -## Actually staking 1 EGLD +[comment]: # (mx-context-auto) -To do this, we simply have to update the value passed when we call the stake function. This should be: -`value=1000000000000000000`. +### But how do I send 0.5 EGLD? -Now let's try staking again: -![img](/developers/staking-contract-tutorial-img/second_stake.png) +Since we know EGLD has 18 decimals, we have to simply multiply 0.5 by 1018, which yields 500000000000000000. [comment]: # (mx-context-auto) -## Querying the view functions +### Actually staking 1 EGLD -To perform smart contract queries, we also use mxpy. Run the next command in a terminal: +To stake 1 EGLD, simply update the `egld_amount` in the `stake` function from `staking-contract/interactor/src/interact.rs` with the following: -```bash - mxpy --verbose contract query ${SC_ADDRESS} \ - --proxy=https://devnet-gateway.multiversx.com \ - --function="getStakingPosition" \ - --arguments ${USER_ADDRESS} +```rust +let egld_amount = BigUint::::from(1000000000000000000u128); ``` -:::note -You don't need a PEM file or an account at all to perform queries. Notice how you also don't need a chain ID for this call. -::: - -:::note -Because there is no PEM file required, there is no "caller" for VM queries. Attempting to use `self.blockchain().get_caller()` in a query function will return the SC's own address. -::: +Now let's try staking again: -Replace `USER_ADDRESS` value with your address. Now let's see our staking amount, according to the SC's internal state: +![img](/developers/staking-contract-tutorial-img/second_stake.png) -```bash -getStakeForAddress -[ - { - "base64": "DeC2s6dkAAE=", - "hex": "0de0b6b3a7640001", - "number": 1000000000000000001 - } -] -``` +[comment]: # (mx-context-auto) -We get the expected amount, 1 EGLD, plus the initial 10^-18 EGLD we sent. +## Performing contract queries -Now let's also query the stakers list: +To perform smart contract queries for the `getStakingPosition` view, update the `addr` variable in the `staking_position` function from `staking-contract/interactor/src/interact.rs` with the address of the wallet you used for the staking transaction: -```bash - mxpy --verbose contract query ${SC_ADDRESS} \ - --proxy=https://devnet-gateway.multiversx.com \ - --function="getStakedAddresses" +```rust +let addr = bech32::decode("erd1vx..."); ``` -Running this function should yield a result like this: +Query the staking position by running the following command in the terminal, inside the `staking-contract/interactor` directory: ```bash -getAllStakers -[ - { - "base64": "nKGLvsPooKhq/R30cdiu1SRbQysprPITCnvi04n0cR0=", - "hex": "9ca18bbec3e8a0a86afd1df471d8aed5245b432b29acf2130a7be2d389f4711d", - "number": 70846231242182541417246304875524977991498122361356467219989042906898688667933 - } -] +cargo run getStakingPosition ``` -...but what's this value? If we try to convert `9ca18bbec3e8a0a86afd1df471d8aed5245b432b29acf2130a7be2d389f4711d` to ASCII, we get gibberish. So what happened to our pretty erd1 address? - -[comment]: # (mx-context-auto) - -### Converting erd1 addresses to hex - -The smart contracts never work with the `erd1...` address format, but rather with the hex format. This is NOT an ASCII to hex conversion. This is a bech32 to ASCII conversion. - -But then, why did the previous query work? +This will show the staking amount according to the smart contract's internal state: ```bash - mxpy --verbose contract query ${SC_ADDRESS} \ - --proxy=${PROXY} \ - --function="getStakingPosition" \ - --arguments ${USER_ADDRESS} +Result: 1000000000000000001 ``` -This is because mxpy automatically detected and converted the erd1 address to hex. To perform those conversions yourself, you can also use mxpy: +The result includes 1 EGLD plus the initial 10-18 EGLD that was sent. -bech32 to hex +Next, query the **stakers list**. Replace the next line from `staked_addresses` **function** in `staking-contract/interactor/src/interact.rs`: -```bash -mxpy wallet bech32 --decode erd1... +```rust +println!("Result: {result_value:?}"); ``` -In the previous example, we used the address: erd1njsch0krazs2s6harh68rk9w65j9kset9xk0yyc2003d8z05wywsmmnn76 - -Now let's try and decode this with mxpy: +with the following: -```bash -mxpy wallet bech32 --decode erd1njsch0krazs2s6harh68rk9w65j9kset9xk0yyc2003d8z05wywsmmnn76 -9ca18bbec3e8a0a86afd1df471d8aed5245b432b29acf2130a7be2d389f4711d +```rust +for result in result_value.iter() { + println!("Result: {}", Bech32Address::from(result).to_bech32_string()); +} ``` -[comment]: # (mx-context) - -Which is precisely the value we received from the smart contract. Now let's try it the other way around. +It is necessary to iterate through `result_value` because it is a [`MultiValueVec`](/docs/developers/data/multi-values.md#standard-multi-values) of `Address`. Each address is converted to `Bech32Address` to ensure it’s printed in Bech32 format, not as raw ASCII. -hex to bech32 +Run in terminal at path `staking-contract/interactor`: ```bash -mxpy wallet bech32 --encode hex_address +cargo run getStakedAddresses ``` -Running the command with the previous example, we should get the same initial address: +Running this function should yield a result like this: ```bash -mxpy wallet bech32 --encode 9ca18bbec3e8a0a86afd1df471d8aed5245b432b29acf2130a7be2d389f4711d -erd1njsch0krazs2s6harh68rk9w65j9kset9xk0yyc2003d8z05wywsmmnn76 +Result: erd1vx... ``` [comment]: # (mx-context-auto) ## Adding unstake functionality -For now, users can only stake, but they cannot actually get their EGLD back... at all. Let's add the unstake endpoint in our SC: +Currently, users can only stake, but they cannot actually get their EGLD back... at all. Let's add the unstake functionality in our smart contract: ```rust #[endpoint] @@ -470,18 +486,18 @@ fn unstake(&self) { self.staked_addresses().swap_remove(&caller); stake_mapper.clear(); - self.send().direct_egld(&caller, &caller_stake); + self.tx().to(caller).egld(caller_stake).transfer(); } ``` -You might notice the variable `stake_mapper`. Just to remind you, the mapper's definition looks like this: +You might notice the `stake_mapper` variable. Just to remind you, the mapper's definition looks like this: ```rust #[storage_mapper("stakingPosition")] fn staking_position(&self, addr: &ManagedAddress) -> SingleValueMapper; ``` -In pure Rust terms, this is a method of our contract trait, with one argument, that returns a `SingleValueMapper`. All mappers are nothing more than struct types that provide an interface to the storage API. +In Rust terms, this is a method of our contract trait, with one argument, that returns a [`SingleValueMapper`](/docs/developers/developer-reference/storage-mappers.md#singlevaluemapper). All [mappers](/docs/developers/developer-reference/storage-mappers.md) are nothing more than structure types that provide an interface to the storage API. So then, why save the mapper in a variable? @@ -523,22 +539,16 @@ fn unstake(&self, unstake_amount: BigUint) { self.staked_addresses().swap_remove(&caller); } - self.send().direct_egld(&caller, &unstake_amount); + self.tx().to(caller).egld(unstake_amount).transfer(); } ``` -As you might notice, the code changed quite a bit. We also need to account for invalid user input, so we add a `require!` statement. Additionally, since we no longer need to simply "clear" the storage, we use the `update` method, which allows us to change the currently stored value through a mutable reference. +As you might notice, the code changed quite a bit: -`update` is the same as doing `get`, followed by computation, and then `set`, but it's just a lot more compact. Additionally, it also allows us to return anything we want from the given closure, so we use that to detect if this was a full unstake. +1. To handle invalid user input, we used the `require!` statement; +2. Previously, we used `clear` to reset the staking position. However, now that we need to modify the stored value, we use the `update` method, which allows us to change the currently stored value through a mutable reference. -```rust -pub fn update R>(&self, f: F) -> R { - let mut value = self.get(); - let result = f(&mut value); - self.set(value); - result -} -``` +[`update`](/docs/developers/developer-reference/storage-mappers.md#update) is the same as doing [`get`](/docs/developers/developer-reference/storage-mappers.md#get), followed by computation, and then [`set`](/docs/developers/developer-reference/storage-mappers.md#set), but it's just a lot more compact. Additionally, it also allows us to return anything we want from the given closure, so we use that to detect if this was a full unstake. [comment]: # (mx-context-auto) @@ -569,7 +579,7 @@ fn unstake(&self, opt_unstake_amount: OptionalValue) { self.staked_addresses().swap_remove(&caller); } - self.send().direct_egld(&caller, &unstake_amount); + self.tx().to(caller).egld(unstake_amount).transfer(); } ``` @@ -577,265 +587,281 @@ This makes it so if someone wants to perform a full unstake, they can simply not [comment]: # (mx-context-auto) -### Unstaking our devnet tokens +### Unstaking our Devnet tokens -Now that we've added the unstake function, let's test it out on devnet. Build your SC again, and add the unstake function to our snippets.rs file: +Now that the unstake function has been added, let's test it out on Devnet. Build the smart contract again. In the contract root (`staking-contract/`), run the next command: ```bash -UNSTAKE_AMOUNT=500000000000000000 - -mxpy --verbose contract call ${SC_ADDRESS} \ - --proxy=https://devnet-gateway.multiversx.com --chain=D \ - --send --recall-nonce --pem=~/MyTestWallets/tutorialKey.pem \ - --gas-limit=10000000 \ - --function="unstake" \ - --arguments ${UNSTAKE_AMOUNT} +sc-meta all build ``` -Now run this function, and you'll get this result: +After building the contract, regenerate the interactor by running the following command in the terminal, also in the contract root: + +```bash +sc-meta all snippets +``` + +:::warning +Make sure `wallet_address` stores the wallet that has to execute the transactions. +::: + +Let's unstake some EGLD! Replace variable `opt_unstake_amount` from `unstake` function in `staking-contract/interactor/src/interact.rs` with: + +```rust +let opt_unstake_amount = OptionalValue::Some(BigUint::::from(500000000000000000u128)); +``` + +Then, run in terminal at path `staking-contract/interactor`: + +```bash +cargo run unstake +``` + +Now run this function, and you'll get this result: + ![img](/developers/staking-contract-tutorial-img/first_unstake.png) -...but why? We just added the function! Well, we might've added it to our code, but the contract on the devnet still has our old code. So, how do we upload our new code? +...but why? We just added the function! + +Well, we might've added it to our code, but the contract on the Devnet still has our old code. So, how do we upload our new code? [comment]: # (mx-context-auto) ## Upgrading smart contracts -Since we've added some new functionality, we also want to update the currently deployed implementation. **Build the contract** and then run the following command: +Since we've added some new functionality, we also want to update the currently deployed implementation. **Build the contract** and then run the following command at path `staking-contract/interactor`: ```bash - mxpy --verbose contract upgrade ${SC_ADDRESS} \ - --bytecode=~/Projects/tutorials/staking-contract/output/staking-contract.wasm \ - --recall-nonce --pem=~/MyTestWallets/tutorialKey.pem \ - --gas-limit=20000000 \ - --send --outfile="upgrade-devnet.interaction.json" \ - --proxy=https://devnet-gateway.multiversx.com --chain=D +cargo run upgrade ``` -:::note -Keep in mind the `#[init]` function of the newly uploaded code is also called on upgrade. For now, it does not matter, as our init function does nothing, but it's worth keeping in mind. -::: - -:::note -All the storage is kept on upgrade, so make sure any storage changes you make to storage mapping are backwards compatible! +:::note Attention required +All the storage is kept on upgrade, so make sure any storage changes you make to storage mapping are **backwards compatible**! ::: [comment]: # (mx-context-auto) ## Try unstaking again -Try running the `unstake` snippet again. This time, it should work just fine. Afterwards, let's query our staked amount through `getStakeForAddress`, to see if it updated our amount properly: +Run the `unstake` snippet again. This time, it should work just fine. Afterwards, let's query our staked amount through `getStakingPosition`, to see if it updated our amount properly. + +:::warning +Make sure that function `staking_position` has the changes previously made. +::: ```bash -getStakeForAddress -[ - { - "base64": "BvBbWdOyAAE=", - "hex": "06f05b59d3b20001", - "number": 500000000000000001 - } -] +cargo run getStakingPosition ``` -We had 1 EGLD, and we've unstaked 0.5 EGLD. Now we have 0.5 EGLD staked. (with the extra 1 fraction of EGLD we've staked initially). +```bash +Result: 500000000000000001 +``` + +We had 1 EGLD, and we unstaked 0.5 EGLD. Now, we have 0.5 EGLD staked. (with the extra 1 fraction of EGLD we've staked initially). [comment]: # (mx-context-auto) -## Unstake with no arguments +### Unstake with no arguments -Let's also test the optional argument functionality. Remove the `--arguments` line from the snippet, and run it again. +Now let’s test how the contract behaves when no amount is explicitly provided. This will confirm that the `OptionalValue::None` path works as expected. -```bash -unstake() { - mxpy --verbose contract call ${SC_ADDRESS} \ - --proxy=https://devnet-gateway.multiversx.com --chain=D \ - --send --recall-nonce --pem=~/MyTestWallets/tutorialKey.pem \ - --gas-limit=10000000 \ - --function="unstake" -} +In `staking-contract/interactor/src/interact.rs`, update the `unstake` function. Replace the `opt_unstake_amount` variable with: + +```rust +let opt_unstake_amount: OptionalValue> = OptionalValue::None; ``` -Let's also query `getStakeForAddress` and `getAllStakers` afterwards to see if the state was cleaned up properly: +And then unstake: ```bash -getStakeForAddress -[ - "" -] +cargo run unstake ``` -```bash -getAllStakers -[] -``` +After unstaking, query `stakingPosition` and `stakedAddresses` to see if the state was cleaned up properly: + +These should show that: -As you can see, we get an empty result (which means the value 0), and an empty array respectively. +- Your staking position is now empty: 0 EGLD; +- Your address has been removed from the stakers list. [comment]: # (mx-context-auto) -## Writing Rust tests +## Writing tests -As you might've noticed, it can be quite a chore to keep upgrading the contract after every little change, especially if all we want to do is test a new feature. So let's recap what we've done until now: +As you might've noticed, it can be quite a chore to keep upgrading the contract after every little change, especially if all we want to do is test a new feature. So, let's recap what we've done until now: - deploy our contract - stake - partial unstake - full unstake -:::note -A more detailed explanation of Rust tests can be found here: https://docs.multiversx.com/developers/testing/rust/sc-test-overview/ +:::tip +A more detailed explanation of Rust tests can be found [here](https://docs.multiversx.com/developers/testing/rust/sc-test-overview/). ::: -To test the previously described scenario, we're going to need a user address, and a new test function. Replace the contents of the `./tests/empty_rust_test.rs` file with the following: +Before developing the tests, you will have to generate the contract's [proxy](/docs/developers/transactions/tx-proxies.md). + +You will add to `staking-contract/sc-config.toml`: + +```toml +[[proxy]] +path = "tests/staking_contract_proxy.rs" +``` + +Then run in terminal at path `staking-contract/`: + +```bash +sc-meta all proxy +``` + +You’ll find the contract’s proxy in `staking-contract/tests`, inside the file `staking_contract_proxy.rs`. This proxy will be used to help us write and run tests for the smart contract. + +To test the previously described scenario, we're going to need a user address, and a new test function. + +**Create** file `staking_contract_blackbox_test.rs` in `staking-contract/tests` with the following: ```rust -use multiversx_sc::{codec::multi_types::OptionalValue, types::Address}; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, rust_biguint, whitebox::*, DebugApi, +mod staking_contract_proxy; +use multiversx_sc::{ + imports::OptionalValue, + types::{TestAddress, TestSCAddress}, }; -use staking_contract::*; +use multiversx_sc_scenario::imports::*; -const WASM_PATH: &'static str = "output/staking-contract.wasm"; +const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); +const STAKING_CONTRACT_ADDRESS: TestSCAddress = TestSCAddress::new("staking-contract"); +const USER_ADDRESS: TestAddress = TestAddress::new("user"); +const WASM_PATH: MxscPath = MxscPath::new("output/staking-contract.mxsc.json"); const USER_BALANCE: u64 = 1_000_000_000_000_000_000; -struct ContractSetup -where - ContractObjBuilder: 'static + Copy + Fn() -> staking_contract::ContractObj, -{ - pub b_mock: BlockchainStateWrapper, - pub owner_address: Address, - pub user_address: Address, - pub contract_wrapper: - ContractObjWrapper, ContractObjBuilder>, +struct ContractSetup { + pub world: ScenarioWorld, } -impl ContractSetup -where - ContractObjBuilder: 'static + Copy + Fn() -> staking_contract::ContractObj, -{ - pub fn new(sc_builder: ContractObjBuilder) -> Self { - let rust_zero = rust_biguint!(0u64); - let mut b_mock = BlockchainStateWrapper::new(); - let owner_address = b_mock.create_user_account(&rust_zero); - let user_address = b_mock.create_user_account(&rust_biguint!(USER_BALANCE)); - let sc_wrapper = - b_mock.create_sc_account(&rust_zero, Some(&owner_address), sc_builder, WASM_PATH); +impl ContractSetup { + pub fn new() -> Self { + let mut world = ScenarioWorld::new(); + world.set_current_dir_from_workspace("staking-contract"); + world.register_contract(WASM_PATH, staking_contract::ContractBuilder); + + world.account(OWNER_ADDRESS).nonce(1).balance(0); + world.account(USER_ADDRESS).nonce(1).balance(USER_BALANCE); // simulate deploy - b_mock - .execute_tx(&owner_address, &sc_wrapper, &rust_zero, |sc| { - sc.init(); - }) - .assert_ok(); - - ContractSetup { - b_mock, - owner_address, - user_address, - contract_wrapper: sc_wrapper, - } + world + .tx() + .from(OWNER_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .init() + .code(WASM_PATH) + .new_address(STAKING_CONTRACT_ADDRESS) + .run(); + + ContractSetup { world } } } #[test] fn stake_unstake_test() { - let mut setup = ContractSetup::new(staking_contract::contract_obj); - let owner_addr = setup.owner_address.clone(); - let user_addr = setup.user_address.clone(); + let mut setup = ContractSetup::new(); setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(USER_BALANCE)); - setup - .b_mock - .check_egld_balance(setup.contract_wrapper.address_ref(), &rust_biguint!(0)); + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); + setup.world.check_account(OWNER_ADDRESS).balance(0); // stake full setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(USER_BALANCE), - |sc| { - sc.stake(); - - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - managed_biguint!(USER_BALANCE) - ); - }, - ) - .assert_ok(); + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .stake() + .egld(USER_BALANCE) + .run(); setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(0)); - setup.b_mock.check_egld_balance( - setup.contract_wrapper.address_ref(), - &rust_biguint!(USER_BALANCE), - ); + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(USER_BALANCE)) + .run(); + + setup.world.check_account(USER_ADDRESS).balance(0); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE); // unstake partial setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(0), - |sc| { - sc.unstake(OptionalValue::Some(managed_biguint!(USER_BALANCE / 2))); - - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - managed_biguint!(USER_BALANCE / 2) - ); - }, - ) - .assert_ok(); + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::Some(USER_BALANCE / 2)) + .run(); setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(USER_BALANCE / 2)); - setup.b_mock.check_egld_balance( - setup.contract_wrapper.address_ref(), - &rust_biguint!(USER_BALANCE / 2), - ); + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(USER_BALANCE / 2)) + .run(); + + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE / 2); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE / 2); // unstake full setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(0), - |sc| { - sc.unstake(OptionalValue::None); - - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - managed_biguint!(0) - ); - }, - ) - .assert_ok(); + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::None::) + .run(); setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(USER_BALANCE)); + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(0u8)) + .run(); + + setup + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); setup - .b_mock - .check_egld_balance(setup.contract_wrapper.address_ref(), &rust_biguint!(0)); + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(0); } ``` -We've added a `user_address` field in the setup struct, which is initiated with `USER_BALANCE` EGLD in their account. +We've added a `USER_ADDRESS` constant, which is initialized with `USER_BALANCE` EGLD in their account. :::note For the test we're going to use small numbers for balances, since there is no reason to work with big numbers. For this test, we're using 1 EGLD for user balance. ::: -Then, we've staked the user's entire balance, unstaked half, then unstaked fully. After each transaction, we've checked the SC's internal staking storage, and also the balance of the user and the SC respectively. +Then, we've staked the user's entire balance, unstaked half, then unstaked fully. After each transaction, we've checked the smart contract's internal staking storage, and also the balance of the user and the smart contract respectively. [comment]: # (mx-context-auto) @@ -847,23 +873,21 @@ To run a test, you can use click on the `Run Test` button from under the test na There is also a `Debug` button, which can be used to debug smart contracts. More details on that [here](/developers/testing/sc-debugging/). -Alternatively, you can run all the tests in the file by running the following command in the VSCode terminal, in the `./staking-contract` folder: +Alternatively, you can run all the tests in the file by running the following command in the terminal, in the `staking-contract/` folder: ```bash -cargo test --test empty_rust_test +sc-meta test ``` -Where `empty_rust_test` is the name of the file containing the tests. - [comment]: # (mx-context-auto) ## Staking Rewards -Right now, there is no incentive to stake EGLD into this smart contract. Let's say we want to give every staker 10% APY (Annual Percentage Yield). For example, if someone staked 100 EGLD, they will receive a total of 10EGLD per year. +Right now, there is no incentive to stake EGLD in this smart contract. Let's say we want to give every staker 10% APY (Annual Percentage Yield). For example, if someone staked 100 EGLD, they will receive a total of 10 EGLD per year. -For this, we're also going to need to save the time at which each user staked. Also, we can't simply make each user wait 1 year to get their rewards. We need a more fine-tuned solution, so we're going to calculate rewards per block instead of per year. +For this, we're also going to need to save the time at which each user staked. Also, we can't simply make each user wait one year to get their rewards. We need a more fine-tuned solution, so we're going to calculate rewards per block instead of per year. -:::note +:::tip You can also use rounds, timestamp, epochs etc. for time keeping in smart contracts, but number of blocks is the recommended approach. ::: @@ -873,7 +897,7 @@ You can also use rounds, timestamp, epochs etc. for time keeping in smart contra A single `BigUint` for each user is not enough anymore. As stated before, we need to also store the stake block, and we need to update this block number on every action. -So we're going to use a struct: +So, we're going to use a struct: ```rust pub struct StakingPosition { @@ -883,7 +907,7 @@ pub struct StakingPosition { ``` :::note -Every managed type from the Rust framework needs a `ManagedTypeApi` implementation, which allows it to access the VM functions for performing operations. For example, adding two `BigUint` numbers, concatenating two `ManagedBuffer`s, etc. Inside smart contract code, the `ManagedTypeApi` associated type is automatically added, but outside of it, we have to manually specify it. +Every managed type from the SpaceCraft needs a `ManagedTypeApi` implementation, which allows it to access the VM functions for performing operations. For example, adding two `BigUint` numbers, concatenating two `ManagedBuffers`, etc. Inside smart contract code, the `ManagedTypeApi` associated type is automatically added, but outside of it, we have to manually specify it. ::: [comment]: # (mx-context) @@ -891,32 +915,33 @@ Every managed type from the Rust framework needs a `ManagedTypeApi` implementati Additionally, since we need to store this in storage, we need to tell the Rust framework how to encode and decode this type. This can be done automatically by deriving (i.e. auto-implementing) these traits, via the `#[derive]` annotation: ```rust -multiversx_sc::derive_imports!(); +use multiversx_sc::derive_imports::*; -#[derive(TypeAbi, TopEncode, TopDecode, PartialEq, Debug)] +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Debug)] pub struct StakingPosition { pub stake_amount: BigUint, pub last_action_block: u64, } ``` -We've also added `TypeAbi`, since this is required for ABI generation. ABIs are used by dApps and such to decode custom SC types, but this is out of scope of this tutorial. +We've also added `#[type_abi]`, since this is required for ABI generation. ABIs are used by decentralized applications and such to decode custom smart contract types, but this is out of scope of this tutorial. Additionally, we've added `PartialEq` and `Debug` derives, for easier use within tests. This will not affect performance in any way, as the code for these is only used during testing/debugging. `PartialEq` allows us to use `==` for comparing instances, while `Debug` will pretty-print the struct, field by field, in case of errors. -If you want to learn more about how such a struct is encoded, and the difference between top and nested encoding/decoding, you can read more [here](/developers/data/serialization-overview): +If you want to learn more about how such a structure is encoded, and the difference between top and nested encoding/decoding, you can read more [here](/developers/data/serialization-overview). [comment]: # (mx-context-auto) ### Rewards formula -A block is produced about every 6 seconds, so total blocks in a year would be seconds in year, divided by 6. More specifically: +A block is produced about every 6 seconds, so total blocks in a year would be seconds in year, divided by 6: ```rust pub const BLOCKS_IN_YEAR: u64 = 60 * 60 * 24 * 365 / 6; ``` -More specifically: 60 seconds per minute _ 60 minutes per hour _ 24 hours per day \* 365 days, divided by the 6-second block duration. +More specifically: *60 seconds per minute* x *60 minutes per hour* x *24 hours per day* x *365 days*, divided by the 6-second block duration. :::note This is calculated and replaced with the exact value at compile time, so there is no performance penalty of having a constant with mathematical operations in its value definition. @@ -930,15 +955,17 @@ Having defined this constant, rewards formula should look like this: let reward_amt = apy / 100 * user_stake * blocks_since_last_claim / BLOCKS_IN_YEAR; ``` -Using 10% as APY, and assuming exactly one year has passed since last claim, the reward amount would be `10/100 * user_stake`, which is exactly 10% APY. +Using 10% as the APY, and assuming exactly one year has passed since last claim. -But there is something wrong with the current formula. We will always get `reward_amt` = 0. +In this case, the reward should be calculated as: `10/100 * user_stake`, which is exactly 10% APY. + +However, there is something wrong with the current formula. We will always get `reward_amt` = 0. [comment]: # (mx-context-auto) ### BigUint division -BigUint division works the same as unsigned integer division. If you divide `x` by `y`, where `x < y`, you will always get 0 as result. So in our previous example, 10/100 is NOT 0.1, but 0. +BigUint division works the same as unsigned integer division. If you divide `x` by `y`, where `x < y`, you will always get `0` as result. So in our previous example, 10/100 is **NOT** `0.1`, but `0`. To fix this, we need to take care of our operation order: @@ -950,40 +977,45 @@ let reward_amt = user_stake * apy / 100 * blocks_since_last_claim / BLOCKS_IN_YE ### How to express percentages like 50.45%? -In this case, we need to extend our precision by using fixed point precision. Instead of having `100` as the max percentage, we will extend it to `10_000`, and give `50.45%` as `5_045`. Updating our above formula results in this: +In this case, we need to extend our precision by using fixed point precision. Instead of having `100` as the maximum percentage, we will extend it to `100_00`, and give `50.45%` as `50_45`. Updating our above formula results in this: ```rust -pub const MAX_PERCENTAGE: u64 = 10_000; +pub const MAX_PERCENTAGE: u64 = 100_00; let reward_amt = user_stake * apy / MAX_PERCENTAGE * blocks_since_last_claim / BLOCKS_IN_YEAR; ``` -For example, let's assume the user stake is 100, and 1 year has passed. Using `5_045` as APY value, the formula would become: +For example, let's assume the user stake is 100, and 1 year has passed. Using `50_45` as APY value, the formula would become: ```rust -reward_amt = 100 * 5_045 / 10_000 = 504_500 / 10_000 = 50 +reward_amt = 100 * 50_45 / 100_00 = 5045_00 / 100_00 = 50 ``` :::note -Since we're still using BigUint division, we don't get `50.45`, but `50`. This precision can be increased by using more zeroes for the MAX_PERCENTAGE and the respective APY, but this is also "inheritly fixed" on the blockchain, because we work with very big numbers for `user_stake` +Since we're still using the BigUint division, we don't get `50.45`, but `50`. This precision can be increased by using more zeroes for the `MAX_PERCENTAGE` and the respective APY, but this is also "inherently fixed" on the blockchain because we work with very big numbers for `user_stake`. ::: [comment]: # (mx-exclude-context) -## Rewards implementation +## Rewards functionality + +[comment]: # (mx-exclude-context) + +### Implementation Now let's see how this would look in our Rust smart contract code. The smart contract looks like this after doing all the specified changes: ```rust #![no_std] -multiversx_sc::imports!(); -multiversx_sc::derive_imports!(); +use multiversx_sc::derive_imports::*; +use multiversx_sc::imports::*; pub const BLOCKS_IN_YEAR: u64 = 60 * 60 * 24 * 365 / 6; pub const MAX_PERCENTAGE: u64 = 10_000; -#[derive(TypeAbi, TopEncode, TopDecode, PartialEq, Debug)] +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Debug)] pub struct StakingPosition { pub stake_amount: BigUint, pub last_action_block: u64, @@ -996,10 +1028,13 @@ pub trait StakingContract { self.apy().set(apy); } + #[upgrade] + fn upgrade(&self) {} + #[payable("EGLD")] #[endpoint] fn stake(&self) { - let payment_amount = self.call_value().egld_value().clone_value(); + let payment_amount = self.call_value().egld().clone(); require!(payment_amount > 0, "Must pay more than 0"); let caller = self.blockchain().get_caller(); @@ -1036,7 +1071,7 @@ pub trait StakingContract { self.staked_addresses().swap_remove(&caller); } - self.send().direct_egld(&caller, &unstake_amount); + self.tx().to(caller).egld(unstake_amount).transfer(); } #[endpoint(claimRewards)] @@ -1059,7 +1094,7 @@ pub trait StakingContract { staking_pos.last_action_block = current_block; if reward_amount > 0 { - self.send().direct_egld(user, &reward_amount); + self.tx().to(user).egld(&reward_amount).transfer(); } } @@ -1098,387 +1133,443 @@ pub trait StakingContract { } ``` -Now, let's update our test, to use our new `StakingPosition` struct, and also provide the `APY` as argument for the `init` function. +Now, rebuild the contract and regenerate the proxy: + +```bash +sc-meta all build +sc-meta all proxy +``` + +Let's update our test, to use our new `StakingPosition` structure, and also provide the `APY` as an argument for the `init` function. ```rust -use multiversx_sc::{codec::multi_types::OptionalValue, types::Address}; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, rust_biguint, whitebox::*, DebugApi, +mod staking_contract_proxy; +use multiversx_sc::{ + imports::OptionalValue, + types::{BigUint, TestAddress, TestSCAddress}, }; -use staking_contract::*; +use multiversx_sc_scenario::imports::*; +use staking_contract_proxy::StakingPosition; -const WASM_PATH: &'static str = "output/staking-contract.wasm"; +const OWNER_ADDRESS: TestAddress = TestAddress::new("owner"); +const USER_ADDRESS: TestAddress = TestAddress::new("user"); +const STAKING_CONTRACT_ADDRESS: TestSCAddress = TestSCAddress::new("staking-contract"); +const WASM_PATH: MxscPath = MxscPath::new("output/staking-contract.mxsc.json"); const USER_BALANCE: u64 = 1_000_000_000_000_000_000; -const APY: u64 = 1_000; // 10% - -struct ContractSetup -where - ContractObjBuilder: 'static + Copy + Fn() -> staking_contract::ContractObj, -{ - pub b_mock: BlockchainStateWrapper, - pub owner_address: Address, - pub user_address: Address, - pub contract_wrapper: - ContractObjWrapper, ContractObjBuilder>, +const APY: u64 = 10_00; // 10% + +struct ContractSetup { + pub world: ScenarioWorld, } -impl ContractSetup -where - ContractObjBuilder: 'static + Copy + Fn() -> staking_contract::ContractObj, -{ - pub fn new(sc_builder: ContractObjBuilder) -> Self { - let rust_zero = rust_biguint!(0u64); - let mut b_mock = BlockchainStateWrapper::new(); - let owner_address = b_mock.create_user_account(&rust_zero); - let user_address = b_mock.create_user_account(&rust_biguint!(USER_BALANCE)); - let sc_wrapper = - b_mock.create_sc_account(&rust_zero, Some(&owner_address), sc_builder, WASM_PATH); +impl ContractSetup { + pub fn new() -> Self { + let mut world = ScenarioWorld::new(); + world.set_current_dir_from_workspace("staking-contract"); + world.register_contract(WASM_PATH, staking_contract::ContractBuilder); + + world.account(OWNER_ADDRESS).nonce(1).balance(0); + world.account(USER_ADDRESS).nonce(1).balance(USER_BALANCE); // simulate deploy - b_mock - .execute_tx(&owner_address, &sc_wrapper, &rust_zero, |sc| { - sc.init(APY); - }) - .assert_ok(); - - ContractSetup { - b_mock, - owner_address, - user_address, - contract_wrapper: sc_wrapper, - } + world + .tx() + .from(OWNER_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .init(APY) + .code(WASM_PATH) + .new_address(STAKING_CONTRACT_ADDRESS) + .run(); + + ContractSetup { world } } } #[test] fn stake_unstake_test() { - let mut setup = ContractSetup::new(staking_contract::contract_obj); - let user_addr = setup.user_address.clone(); + let mut setup = ContractSetup::new(); setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(USER_BALANCE)); - setup - .b_mock - .check_egld_balance(setup.contract_wrapper.address_ref(), &rust_biguint!(0)); + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); + setup.world.check_account(OWNER_ADDRESS).balance(0); // stake full setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(USER_BALANCE), - |sc| { - sc.stake(); - - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - StakingPosition { - stake_amount: managed_biguint!(USER_BALANCE), - last_action_block: 0 - } - ); - }, - ) - .assert_ok(); + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .stake() + .egld(USER_BALANCE) + .run(); + + let expected_result_1 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE), + last_action_block: 0, + }; setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(0)); - setup.b_mock.check_egld_balance( - setup.contract_wrapper.address_ref(), - &rust_biguint!(USER_BALANCE), - ); + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(expected_result_1)) + .run(); + + setup.world.check_account(USER_ADDRESS).balance(0); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE); // unstake partial setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(0), - |sc| { - sc.unstake(OptionalValue::Some(managed_biguint!(USER_BALANCE / 2))); - - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - StakingPosition { - stake_amount: managed_biguint!(USER_BALANCE / 2), - last_action_block: 0 - } - ); - }, - ) - .assert_ok(); + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::Some(USER_BALANCE / 2)) + .run(); + + let expected_result_2 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE / 2), + last_action_block: 0, + }; + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(expected_result_2)) + .run(); + + let staked_addresses_1 = setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staked_addresses() + .returns(ReturnsResultUnmanaged) + .run(); + + assert!(staked_addresses_1 + .into_vec() + .contains(&USER_ADDRESS.to_address())); setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(USER_BALANCE / 2)); - setup.b_mock.check_egld_balance( - setup.contract_wrapper.address_ref(), - &rust_biguint!(USER_BALANCE / 2), - ); + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE / 2); + setup + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(USER_BALANCE / 2); // unstake full setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(0), - |sc| { - sc.unstake(OptionalValue::None); - - assert!(sc - .staking_position(&managed_address!(&user_addr)) - .is_empty()); - }, - ) - .assert_ok(); + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .unstake(OptionalValue::None::) + .run(); + + let staked_addresses_2 = setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staked_addresses() + .returns(ReturnsResultUnmanaged) + .run(); + assert!(staked_addresses_2.is_empty()); setup - .b_mock - .check_egld_balance(&user_addr, &rust_biguint!(USER_BALANCE)); + .world + .check_account(USER_ADDRESS) + .balance(USER_BALANCE); setup - .b_mock - .check_egld_balance(setup.contract_wrapper.address_ref(), &rust_biguint!(0)); + .world + .check_account(STAKING_CONTRACT_ADDRESS) + .balance(0); } ``` -Now let's run the test... it didn't work. You should see the following error: +Now let's run the test... **it didn't work**. You should see the following error: -[comment]: # (mx-context-auto) +```bash +Error: result code mismatch. +Tx id: '' +Want: "0" +Have: 4 +Message: storage decode error (key: stakingPositionuser____________________________): input too short +``` -### Storage decode error: input too short +But why? Everything worked fine before. -But why? Everything worked fine before. This is because instead of using a simple `BigUint` for staking positions, we now use the `StakingPosition` struct. If you follow the error trace, you will see exactly where it failed: +This is because instead of using a simple `BigUint` for staking positions, we now use the `StakingPosition` structure. If you follow the error trace, you will see exactly where it failed: -``` -17: staking_contract::StakingContract::stake - at ./src/empty.rs:29:9 +```bash +28: staking_contract::StakingContract::stake + at ./src/staking_contract.rs:33:9 ``` Which leads to the following line: ```rust self.staking_position(&caller).update(|staking_pos| { - self.claim_rewards_for_user(&caller, staking_pos); + self.claim_rewards_for_user(&caller, staking_pos); - staking_pos.stake_amount += payment_amount - }); + staking_pos.stake_amount += payment_amount +}); ``` -Because we're trying to add a new user, which has no staking entry yet, the decoding fails. For a simple `BigUint`, decoding from an empty storage yields the `0` value, which is exactly what we want, but for a struct type, it cannot give us any default value. +Because we're trying to add a new user, which has no staking entry yet, the decoding fails. + +For a simple `BigUint`, decoding from an empty storage yields the `0` value, which is exactly what we want, but for a struct type, it cannot give us any default value. For this reason, we have to add some additional checks. The endpoint implementations will have to be changed to the following (the rest of the code remains the same): ```rust - #[payable("EGLD")] - #[endpoint] - fn stake(&self) { - let payment_amount = self.call_value().egld_value().clone_value(); - require!(payment_amount > 0, "Must pay more than 0"); +#[payable("EGLD")] +#[endpoint] +fn stake(&self) { + let payment_amount = self.call_value().egld().clone(); + require!(payment_amount > 0, "Must pay more than 0"); - let caller = self.blockchain().get_caller(); - let stake_mapper = self.staking_position(&caller); + let caller = self.blockchain().get_caller(); + let stake_mapper = self.staking_position(&caller); - let new_user = self.staked_addresses().insert(caller.clone()); - let mut staking_pos = if !new_user { - stake_mapper.get() - } else { - let current_block = self.blockchain().get_block_epoch(); - StakingPosition { - stake_amount: BigUint::zero(), - last_action_block: current_block, - } - }; + let new_user = self.staked_addresses().insert(caller.clone()); + let mut staking_pos = if !new_user { + stake_mapper.get() + } else { + let current_block = self.blockchain().get_block_epoch(); + StakingPosition { + stake_amount: BigUint::zero(), + last_action_block: current_block, + } + }; - self.claim_rewards_for_user(&caller, &mut staking_pos); - staking_pos.stake_amount += payment_amount; + self.claim_rewards_for_user(&caller, &mut staking_pos); + staking_pos.stake_amount += payment_amount; + + stake_mapper.set(&staking_pos); +} + +#[endpoint] +fn unstake(&self, opt_unstake_amount: OptionalValue) { + let caller = self.blockchain().get_caller(); + self.require_user_staked(&caller); + + let stake_mapper = self.staking_position(&caller); + let mut staking_pos = stake_mapper.get(); + let unstake_amount = match opt_unstake_amount { + OptionalValue::Some(amt) => amt, + OptionalValue::None => staking_pos.stake_amount.clone(), + }; + require!( + unstake_amount > 0 && unstake_amount <= staking_pos.stake_amount, + "Invalid unstake amount" + ); + + self.claim_rewards_for_user(&caller, &mut staking_pos); + staking_pos.stake_amount -= &unstake_amount; + + if staking_pos.stake_amount > 0 { stake_mapper.set(&staking_pos); + } else { + stake_mapper.clear(); + self.staked_addresses().swap_remove(&caller); } - #[endpoint] - fn unstake(&self, opt_unstake_amount: OptionalValue) { - let caller = self.blockchain().get_caller(); - self.require_user_staked(&caller); + self.tx().to(caller).egld(unstake_amount).transfer(); +} - let stake_mapper = self.staking_position(&caller); - let mut staking_pos = stake_mapper.get(); +#[endpoint(claimRewards)] +fn claim_rewards(&self) { + let caller = self.blockchain().get_caller(); + self.require_user_staked(&caller); - let unstake_amount = match opt_unstake_amount { - OptionalValue::Some(amt) => amt, - OptionalValue::None => staking_pos.stake_amount.clone(), - }; - require!( - unstake_amount > 0 && unstake_amount <= staking_pos.stake_amount, - "Invalid unstake amount" - ); + let stake_mapper = self.staking_position(&caller); - self.claim_rewards_for_user(&caller, &mut staking_pos); - staking_pos.stake_amount -= &unstake_amount; + let mut staking_pos = stake_mapper.get(); + self.claim_rewards_for_user(&caller, &mut staking_pos); - if staking_pos.stake_amount > 0 { - stake_mapper.set(&staking_pos); - } else { - stake_mapper.clear(); - self.staked_addresses().swap_remove(&caller); - } + stake_mapper.set(&staking_pos); +} - self.send().direct_egld(&caller, &unstake_amount); - } +fn require_user_staked(&self, user: &ManagedAddress) { + require!(self.staked_addresses().contains(user), "Must stake first"); +} +``` - #[endpoint(claimRewards)] - fn claim_rewards(&self) { - let caller = self.blockchain().get_caller(); - self.require_user_staked(&caller); +For the `stake` endpoint, if the user was not previously staked, we provide a default entry. The `insert` method of `UnorderedSetMapper` returns `true` if the entry is new and `false` if the user already exists in the list. This means we can use that result directly, instead of checking for `stake_mapper.is_empty()`. - let stake_mapper = self.staking_position(&caller); - let mut staking_pos = stake_mapper.get(); - self.claim_rewards_for_user(&caller, &mut staking_pos); +For the `unstake` and `claimRewards` endpoints, we have to check if the user was already staked and return an error otherwise (as they'd have nothing to unstake/claim anyway). - stake_mapper.set(&staking_pos); - } +Once you've applied all the suggested changes, **rebuilt the contract** and **regenerate the proxy**, running the test should work just fine now: - fn require_user_staked(&self, user: &ManagedAddress) { - require!(self.staked_addresses().contains(user), "Must stake first"); - } +```bash +running 1 test +test stake_unstake_test ... ok ``` -For the `stake` endpoint, in case the user was not previously staked, we provide a default entry. The `insert` method of `UnorderedSetMapper` returns `true` if the entry is new, `false` if the user was already in the list, so we can use that result instead of checking for `stake_mapper.is_empty()`. +In order to apply these changes on devnet, you should build the contract, regenerate the interactor and then upgrade it. -For the `unstake` and `claimRewards` endpoints, we have to check if the user was already staked, and return an error otherwise (as they'd have nothing to unstake/claim anyway). +:::warning +Whenever you regenerate the interactor, make sure that wallet_address points to the wallet you intend to use for executing transactions. -Running the test after the suggested changes should work just fine now: +By default, the interactor registers Alice's wallet, so you’ll need to update wallet_address manually if you’re using a different one. +::: +```bash +sc-meta all build +sc-meta all snippets +cd interactor/ ``` -running 1 test -test stake_unstake_test ... ok + +Set the `apy` variable inside the `upgrade` function from `staking-contract/interactor/src/interact.rs` to `100`, then run: + +```bash +cargo run upgrade +``` + +To verify the change, query the `apy` storage mapper by running: + +```bash +cargo run getApy ``` -In order to apply these changes on devnet, you should build the contract and then upgrade it. Because we added the APY on init now for the upgrade we will have to pass it as an argument. +You should see the following output: ```bash - mxpy --verbose contract upgrade ${SC_ADDRESS} \ - --bytecode=~/Projects/tutorials/staking-contract/output/staking-contract.wasm \ - --recall-nonce --pem=~/MyTestWallets/tutorialKey.pem \ - --gas-limit=20000000 \ - --send --outfile="upgrade-devnet.interaction.json" \ - --proxy=https://devnet-gateway.multiversx.com --chain=D \ - --arguments 100 +Result: 100 ``` [comment]: # (mx-context-auto) -### Rewards testing +### Testing -Now that we've implemented rewards logic, let's add the following test to ensure everything works as expected: +Now that we've implemented rewards logic, let's add the following test to `staking-contract/tests/staking_contract_blackbox_test.rs`: ```rust #[test] fn rewards_test() { - let mut setup = ContractSetup::new(staking_contract::contract_obj); - let user_addr = setup.user_address.clone(); + let mut setup = ContractSetup::new(); // stake full setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(USER_BALANCE), - |sc| { - sc.stake(); - - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - StakingPosition { - stake_amount: managed_biguint!(USER_BALANCE), - last_action_block: 0 - } - ); - }, - ) - .assert_ok(); - - setup.b_mock.set_block_nonce(BLOCKS_IN_YEAR); + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .stake() + .egld(USER_BALANCE) + .run(); + + let expected_result_1 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE), + last_action_block: 0, + }; + + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(&expected_result_1)) + .run(); + + setup.world.current_block().block_nonce(BLOCKS_IN_YEAR); // query rewards setup - .b_mock - .execute_query(&setup.contract_wrapper, |sc| { - let actual_rewards = sc.calculate_rewards_for_user(managed_address!(&user_addr)); - let expected_rewards = managed_biguint!(USER_BALANCE) * APY / MAX_PERCENTAGE; - assert_eq!(actual_rewards, expected_rewards); - }) - .assert_ok(); + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .calculate_rewards_for_user(USER_ADDRESS) + .returns(ExpectValue( + BigUint::from(USER_BALANCE) * APY / MAX_PERCENTAGE, + )) + .run(); // claim rewards setup - .b_mock - .execute_tx( - &user_addr, - &setup.contract_wrapper, - &rust_biguint!(0), - |sc| { - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - StakingPosition { - stake_amount: managed_biguint!(USER_BALANCE), - last_action_block: 0 - } - ); - - sc.claim_rewards(); - - assert_eq!( - sc.staking_position(&managed_address!(&user_addr)).get(), - StakingPosition { - stake_amount: managed_biguint!(USER_BALANCE), - last_action_block: BLOCKS_IN_YEAR - } - ); - }, - ) - .assert_ok(); - - setup.b_mock.check_egld_balance( - &user_addr, - &(rust_biguint!(USER_BALANCE) * APY / MAX_PERCENTAGE), - ); + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(&expected_result_1)) + .run(); + + setup + .world + .tx() + .from(USER_ADDRESS) + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .claim_rewards() + .run(); + + let expected_result_2 = StakingPosition { + stake_amount: BigUint::from(USER_BALANCE), + last_action_block: BLOCKS_IN_YEAR, + }; + setup + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .staking_position(USER_ADDRESS) + .returns(ExpectValue(expected_result_2)) + .run(); + + setup + .world + .check_account(USER_ADDRESS) + .balance(BigUint::from(USER_BALANCE) * APY / MAX_PERCENTAGE); // query rewards after claim setup - .b_mock - .execute_query(&setup.contract_wrapper, |sc| { - let actual_rewards = sc.calculate_rewards_for_user(managed_address!(&user_addr)); - let expected_rewards = managed_biguint!(0); - assert_eq!(actual_rewards, expected_rewards); - }) - .assert_ok(); + .world + .query() + .to(STAKING_CONTRACT_ADDRESS) + .typed(staking_contract_proxy::StakingContractProxy) + .calculate_rewards_for_user(USER_ADDRESS) + .returns(ExpectValue(0u32)) + .run(); } ``` In the test, we perform the following steps: -- stake 1 EGLD -- set block nonce after 1 year (i.e. simulating 1 year worth of blocks passing) -- querying rewards, which should give use 10% of 1 EGLD = 0.1 EGLD -- claiming said rewards and checking the internal state and user balance -- querying again after claim, to check that double-claim is not possible +- Stake 1 EGLD; +- Set block nonce after 1 year (i.e. simulating 1 year worth of blocks passing); +- Querying rewards, which should give use 10% of 1 EGLD = 0.1 EGLD; +- Claiming said rewards and checking the internal state and user balance; +- Querying again after claim, to check that double-claim is not possible. This test should work without any errors. [comment]: # (mx-context-auto) -## Depositing rewards / Conclusion +## Conclusion -Currently, there is no way to deposit rewards into the SC, unless the owner makes it payable, which is generally bad practice, and not recommended. +Currently, there is no way to deposit rewards into the smart contract, unless the owner makes it payable, which is generally bad practice, and not recommended. As this is a fairly simple task compared to what we've done already, we'll leave this as an exercise to the reader. You'll have to add a `payable("EGLD")` endpoint, and additionally, a storage mapper that keeps track of the remaining rewards. Good luck! - -In part 2, which will come soon, we'll discuss about how to use custom ESDTs instead of just EGLD. diff --git a/static/developers/staking-contract-tutorial-img/external_faucet.png b/static/developers/staking-contract-tutorial-img/external_faucet.png index 8d7ccf046..54f1cbb23 100644 Binary files a/static/developers/staking-contract-tutorial-img/external_faucet.png and b/static/developers/staking-contract-tutorial-img/external_faucet.png differ diff --git a/static/developers/staking-contract-tutorial-img/first_stake.png b/static/developers/staking-contract-tutorial-img/first_stake.png index dbf149873..a922cb7df 100644 Binary files a/static/developers/staking-contract-tutorial-img/first_stake.png and b/static/developers/staking-contract-tutorial-img/first_stake.png differ diff --git a/static/developers/staking-contract-tutorial-img/first_unstake.png b/static/developers/staking-contract-tutorial-img/first_unstake.png index e67dc541d..9c97c2ad0 100644 Binary files a/static/developers/staking-contract-tutorial-img/first_unstake.png and b/static/developers/staking-contract-tutorial-img/first_unstake.png differ diff --git a/static/developers/staking-contract-tutorial-img/second_stake.png b/static/developers/staking-contract-tutorial-img/second_stake.png index 32ad52909..6412db054 100644 Binary files a/static/developers/staking-contract-tutorial-img/second_stake.png and b/static/developers/staking-contract-tutorial-img/second_stake.png differ diff --git a/static/developers/staking-contract-tutorial-img/too_much_gas.png b/static/developers/staking-contract-tutorial-img/too_much_gas.png index 8ae6c574b..e12fdec03 100644 Binary files a/static/developers/staking-contract-tutorial-img/too_much_gas.png and b/static/developers/staking-contract-tutorial-img/too_much_gas.png differ diff --git a/static/developers/staking-contract-tutorial-img/wallet_faucet.png b/static/developers/staking-contract-tutorial-img/wallet_faucet.png index c3411deef..706b8ac23 100644 Binary files a/static/developers/staking-contract-tutorial-img/wallet_faucet.png and b/static/developers/staking-contract-tutorial-img/wallet_faucet.png differ