Simulating a bank with a smart contract on TON blockchain. Part II

surprise smart contract

In my previous post I described that I wanted to implement a smart contract that would simulate the basic functionalities of a bank. For a full description of those operations, I strongly recommend you to take a look at the Part I of these posts.

As I promise, I’ll give now in this post, a basic overview of the code. This helped me a lot in knowing how FunC works, and common pitfalls that you will encounter if you don’t take clear caution. I’ll describe two of them. It took me a lot of time to figure out the reason, and the reason was so simple as the absence of an impure specifier. This error gives me a lot of headache, but let’s go to the description and analysis of the code.

Requirements

The repository containing this code can be found in Gealber/blog_recipes/bank_contract. My only intention with this post is to analyze the code, so there won’t be any requirements. Nevertheless, it is important to know how to set up your env to run code from someone else. If you are just starting with TON, I recommend you to take a look at the Hello World tutorial. This tutorial will give you a better understanding on what is needed, and in general it’s a better tutorial overall. In case you are wondering what you will need to deploy this smart contract in specific, I recommend you to take a look at these files in the repository:

  1. deploy.sh
  2. scripts/compile.sh
  3. main.go

Structure

As always it is quite useful to have an understanding of the project structure in order to be able to read someone else’s code. My advice is always to look for the main thread, that in our case, given that it’s a smart contract, our main thread will be that method that handles inbound messages. In the TON blockchain this method is called recv_internal. Making a simple

grep recv_internal -rn --include=*.fc

You should receive the following output:

contracts/bank.fc:1:() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure

Which tells us that this method is located in contracts/bank.fc line 1, so let’s start with this file. In cases where the content of a method is too large I’ll avoid displaying all its content here, you can always check it out in the source code.

file: bank_contract/bank.fc

() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure {
    ;; start parsing the cell
    var cs = in_msg_cell.begin_parse();
    ;; load the following flags: int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
    var flags = cs~load_uint(4);
    ;; load sender address
    slice s_addr = cs~load_msg_addr(); 

    if (slice_empty?(in_msg)) {
        return ();
    }


    ;; parsing address and checking its workchain
    var address = parse_work_addr(s_addr);    
    process_client_request(address, flags, msg_value, in_msg);
}

recv_internal definition

() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure

If you take a look, recv_internal will be able to receive 3 arguments:

  1. The first one is the msg_value that is not other than the amount of TONS that were sent on the message.
  2. in_msg_cell contains the inbound message as a cell.
  3. in_msg contains the body of this inbound message as a slice.

There’s another word in the definition that is quite important, which is the impure specifier.

The method starts parsing a Cell, which is the main structure in the TON blockchain where data is stored. Honestly I don’t have a full grasp yet on how Cells are serialized and so on, and I don’t think that is necessary to start tinkering with them. Keep in mind that we store up to 1023 bits on them, and can have 4 references to other cells. In case we will need to store more than 1023 bits, you create another cell, and reference the previous one to this new one. Again this is not needed now.

    ;; previous code

    ;; start parsing the cell
    var cs = in_msg_cell.begin_parse();
    ;; load the following flags: int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
    var flags = cs~load_uint(4);
    ;; load sender address
    slice s_addr = cs~load_msg_addr();

In this block of code we parse the cell, to extract the flags and the sender address. The flags will tell us later if this message is not a result of a bounce.Next we have a call to

process_client_request(address, flags, msg_value, in_msg);

we need to figure out what this does. Making a simple search like in the other time you can notice that process_client_request is defined in the requests.fc file. This method will handle the operation codes, send in the message body(in_msg), and dispatch handle_deposit, handle_withdraw, and handle_borrow accordingly. Take a look at the following snippet:

    int op = in_msg~load_uint(32);
    if (op == op::deposit()) {
        ;; handle deposit of funds by the user        
        handle_deposit(address, msg_value);
    }

    if (op == op::borrow()) {
        ;; handle borrow of money by the user
        ;; fixed amount to borrow, 1% of the current balance
        handle_borrow(address);        
    }

    if (op == op::withdraw()) {
        ;; handle withdraw of user funds
        handle_withdraw(address, in_msg);
    }

Until now we have the following flow

flow

With this general idea of the structure of the code, you now can deep dive into the code. Important to notice that smart contracts in TON are a thing, as it is described in the documentation, they have address, data, balance, etc…basically an object. Each event is handled one by one, following the Actor pattern.

Common pitfalls

TVM Exit Code 7

If you take a look at the repository, you can notice the presence of a file called errors.history.txt. This file contains two of the errors that I encountered while developing this smart contract, there were more but I think these two were the more hard to debug. One of them I still don’t know why happens, obviously this is due to lack of knowledge on my part.

The one that was really a pain in the ass, was the one related to TVM Exit Code 7, now this error according to the doc.

Type check error. An argument to a primitive is of an incorrect value type.

In my case this was happening in a line where I was modifying a global variable, which wasn’t initialized by the load() method. The problem was that I didn’t specify the impure specifier at the end of this method definition, neither in store_base_data(). As long as the method modifies the contract storage, like in these cases, you should put this specifier. There are other cases as well that I strongly recommend you to check in the documentation.

Develop without testing

In personal projects we as lazy people try to undermine the importance of testing, and we always leave it to the end, or don’t do it at all. The issue is that developing a smart contract without testing it properly, from the beginning to the end, costs you money. My personal advice, try to set up your testing environment and make use of the ton-contract-executor. Extremely useful, this project, awesome. You can also check out blueprint, which I haven’t used, but it’s recommended by the official documentation.

Advice

Don’t get scared of TVM Assembly

Many times you will encounter definitions in the Standard Library, written in TVM Assembly. Also in the logging of ton-contract-executor what you will get will be Assembly as well. Don’t be scared.

Conclusion

In general I think it was quite a good experiment, it’s not a perfect code, not a perfect idea either, quite hackable actually. I learned a lot. I strongly recommend that if you want to learn something, go and try to build something on it, even if it’s quite a silly idea.