Reading smart contract and learning from it

FunC is it really fun?

The goal

Hi there folks 👋, today I’ll be sharing with you my adventure on reading tonwhales nominator smart contract, and what I learned from it. Given the extensiveness of the smart contract, and that I’m completely new to FunC, I’ll only analyze in this post the deposit operation on this contract. I’ll also share with you my attempt to craft a request by myself to interact with this smart contract, and what my mistakes were. Really a valuable lesson for someone who is starting in blockchain.

Let’s see what I have for you, feel free to reach me out on Linkedin for any question you might have.

TL;DR

In order to save you time, you can go directly to this part. If you are only interested in the crafted message.

FunC is it really fun?

I hate to find myself saying this, but C after all wasn’t so bad 🤐. To give you a little of context, FunC is the official language for writing smart contracts on the Telegram Open Network(TON). It’s kind of a pain in the ass to be honest, but it is the tool that we have, so we don’t complain, instead we try to figure out how we can use it. Every language has its own drawbacks, there’s no such a thing as a perfect programming language. Enough with the complains, let me show what I’ve learned reading tonwhales nominator smart contract.

Basic concepts

Validators and nominators

First of all, TON uses Proof of stake consensus, the validation of the blocks in the network are performed by the validators. These are just participants in the network that validate blocks and get Toncoin in return. To be a validator, requires access to a very nice hardware and not only that, also requires a lot of Toncoin. You can read more about it on the official website.

While a nominator is a network participant who lends his/her assets to validators, and of course nothing of this is done for free, they also get their cut of the pie.

The smart contract code

As a curious person that I am, I decided that it was a good idea to know what is inside this smart contract, and tried to understand how this works. Previously I’ve managed to deploy two silly smart contracts, of which I’m not proud about it 😂, just to know how things work. It’s like when you do the Hello World!! When you are learning a new language, you do it to learn how to compile your code, get familiar with error messages, etc…I suggest you do the same.

My goal

Remember that my goal here is to be able to send a transaction to this smart contract, and that it accepts this transaction as a deposit for staking. I’m going to craft that message using tonutils-go library in Golang.

On what to focus?

Given my little experience, I decided to focus on one particular operation in this smart contract, and only on this one. The operation that I decided to focus on was in the deposit operation. This operation is the one that handles the deposits from a wallet that wants to start staking under this contract.

Deposit operation

As my goal is to craft a transaction that allows me to deposit tons for staking, I need to start reading the recv_internal. This method in Ton’s smart contracts is the one that processes inbound messages, so we need to look at it. Here is part of the code:

file: sources/nominators.fc

() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure {
    ;; ….
    ;; other code goes here
    ;; this is not important to us for the moment
    ;; ….

    ;; Nominators
    var address = parse_work_addr(s_addr);
    op_nominators(address, flags, msg_value, in_msg);
}

In FunC, comments are written with ;;. Here we can notice that fortunately the developers leave us a useful comment that says Nominators, and it seems that the method we need to look at is op_nominators. So let’s see what’s inside of it. Here is part of the code:

file: sources/modules/op-nominators.fc

() op_nominators(int member, int flags, int value, slice in_msg) impure {
    ;; Ignore bounced
    if (flags & 1) {
        return ();
    }

    ;; Check value
    throw_unless(error::invalid_message(), value >= params::min_op());

    ;; Parse operation
    int op = in_msg~load_uint(32);
    int is_text = op == 0;
    if (op == 0) {

        ;; Query ID
        ctx_query_id = 0;

        ;; Op
        op = in_msg~parse_text_command();
    } else {

        ;; Query ID
        int query_id = in_msg~load_uint(64);
        ctx_query_id = query_id;
        throw_unless(error::invalid_message(), ctx_query_id > 0);

        ;; Gas Limit
        int gas_limit = in_msg~load_coins();
        set_gas_limit(gas_limit);
    }

    ;; Deposit stake
    if (op == op::stake_deposit()) {
        op_deposit(member, value);
        return ();
    }

    ;; …
    ;; other operations as withdraw follows bellow this code
    ;; …

    ;; Unknown message
    throw(error::invalid_message());
}

Message parts

Reading this code you can notice that there are three main parts to focus on:

  1. The value, which is not other than the amount of toncoins in the transaction. Should be value >= params::min_op(), so we will need to know what params::min_op() is?
  2. Will also need to add an op, which stands for operation. We have two options for doing so, one, if we provide op = 0 then we should send a comment in our transaction, with the following text Deposit. This can be seen in the parse_text_command() method. The other option is to provide the op with the value of op::stake_deposit(). Which can be found in sources/modules/constants.fc. The value is 2077040623 or 0x7bcd1fef in hex representation. In this case we should also provide a query_id different from 0. This is checked in sources/modules/op-nominators.fc.

Only one thing is left to analyze in order to craft this message properly. Well, this is what I thought at first. We need to look into the code of op_deposit.

op_deposit

The code:


() op_deposit(int member, int value) impure {

    ;; Read extras
    var (enabled, udpates_enabled, min_stake, deposit_fee, withdraw_fee, pool_fee, receipt_price) = ctx_extras;
    throw_unless(error::invalid_message(), enabled);


    ;; Read stake value
    int fee = receipt_price + deposit_fee;
    int stake = value - fee;
    throw_unless(error::invalid_stake_value(), stake >= min_stake);

    ;; Load nominators
    load_member(member);

    ;; Add deposit
    member_stake_deposit(stake);

    ;; Resolve address
    var address = ctx_owner;
    if (member != owner_id()) {
        address = serialize_work_addr(member);
    }

    ;; Send receipt
    if (ctx_query_id == 0) {
        send_text_message(
            address,
            receipt_price,
            send_mode::default(),
            begin_cell()
                .store_accepted_stake(stake)
        );
    } else {
        send_empty_std_message(
            address,
            receipt_price,
            send_mode::default(),
            op::stake_deposit::response(),
            ctx_query_id
        );
    }

    ;; Persist
    store_member();
    store_base_data();
}

Here a quite important part of the code is this check

    ;; Read stake value
    int fee = receipt_price + deposit_fee;
    int stake = value - fee;
    throw_unless(error::invalid_stake_value(), stake >= min_stake);

It’s important to notice that the amount of Toncoins we are going to send in the crafted message, should be value >= min_stake + receipt_price + deposit_fee

Otherwise we will get the error error::invalid_stake_value, which is also defined in sources/modules/constants.fc.

The other important part here is that the smart contract will send us a receipt, if we send an op different than 0 it will be an empty message without comment. Otherwise should be a message with the following comment: Stake <amount sent> accepted

Crafting the message

Now that we have everything to craft this message, here is where our code comes into play.


package main

import (
	"context"
	"strings"
	"github.com/xssnick/tonutils-go/address"
	"github.com/xssnick/tonutils-go/tlb"
	"github.com/xssnick/tonutils-go/tvm/cell"
	"github.com/xssnick/tonutils-go/liteclient"
	"github.com/xssnick/tonutils-go/ton"
	"github.com/xssnick/tonutils-go/ton/wallet"
)

const (
	MainnetConfig = "https://ton-blockchain.github.io/global.config.json"
    TonKeeperQueue1 = "EQAA_5_dizuA1w6OpzTSYvXhvUwYTDNTW_MZDdZ0CGKeeper"
	WalletVersion = wallet.V4R2

)

func main() {
	client := liteclient.NewConnectionPool()
	ctx := context.Background()
	err := client.AddConnectionsFromConfigUrl(ctx, MainnetConfig)

	if err != nil {
		panic(err)
	}

	api := ton.NewAPIClient(client)
	seed := strings.Split("<YOUR SEED>", " ")
	w, err := wallet.FromSeed(api, seed, WalletVersion)
	if err != nil {
		panic(err)
	}

	// TonKeeper QUEUE #1	
	addr, err := address.ParseAddr(TonKeeperQueue1)
	if err != nil {
		panic(err)
	}

	// building the payload
	// here we specify the operation equal to zero
	payload := cell.BeginCell().
		MustStoreUInt(0, 32). // op = 0
        // given that we are sending op = 0, we must include the message 'Deposit'.
        // 'Deposit' converted into hex is '0x4465706f736974' and this into uint 19251831996901748
        MustStoreUInt(19251831996901748, 56).
		EndCell()
	// impostant here to set bounce as false

	msg := &wallet.Message{
		Mode: 1,
		InternalMessage: &tlb.InternalMessage{
			IHRDisabled: true,
			Bounce:      true, // this was edited, in case we face an error, at least we recover back the funds.
			DstAddr:     addr,
			Amount:      tlb.MustFromTON("50"),
			Body:        payload,
		},
	}

	err = w.Send(ctx, msg, false) // not waiting for confirmation
	if err != nil {
		panic(err)
	}
}

The value for the transaction should be setted to 50 Toncoins, let’s see why, to avoid the mistakes I made at first. So if you remember in the op_deposit method we had this:

    ;; Read stake value
    int fee = receipt_price + deposit_fee;
    int stake = value - fee;
    throw_unless(error::invalid_stake_value(), stake >= min_stake);

The values for receipt_price, deposit_fee and min_stake are:

receipt_price = 0.1
deposit_fee = 0.1
min_stake = 49.8

These values can and should be obtained with the get_params(). One of my terrible mistakes at first was to think that these values were the ones defined in the global variables. You can consult this method using TON verifier, on the getters section, look for this method and make a request.

That’s all

I think this is all folks, I really recommend you to read all the smart contracts that you interact with. At the end of the day it is your money that is involved in these smart contracts. You can also check this address EQC9n6aFb2oxQPMTPrHOnZDFcvvC2YLYIgBUms2yAB_LcAtv on Tonviewer, more specific to this transaction that was made interacting with this contract. Feel free also to donate to this address 🙂, I just spent more than 50 TONs trying to make this workout.

Lessons

  1. FunC is not that fun, there’s something quite annoying and is that you can declare variables using a wide variety of characters. Which can be quite inconvenient when you are reading variables like error::invalid_stake_value, where you can get the wrong idea that :: is some kind operator, which is not. Or the case when a method is declared ending in ?, like in udict_get? from stdlib, which gives you the idea that this is similar to JavaScript or C conditional operators. Which by the way is also allowed in FunC in this way:

all ? op::stake_withdraw::response() : op::stake_withdraw::delayed()

In general it is quite confusing, you have to adapt to the idea that variables and functions can contain this kind of character.

  1. This lesson came from one terrible mistake on my part. Obviously because I do not have too much knowledge in blockchain yet. I assumed that the value for the variable params::min_stake() would be the one defined in sources/modules/constants.go. Nothing more far from the truth, the issue is that in smart contracts we use the blockchain as a storage. So its actual value is stored in the blockchain, and is loaded from it every time we need it. Taking a look at sources/modules/store-base.fc you can notice that params::min_stake() comes from the blockchain.