This is the second article in our series on integrating payment channels on Telegram Open Network. In the first part, we introduced the network, detailed our experience of the contest, and explained how synchronous and asynchronous smart contracts work. As the next addition to the series, this article details how we built a synchronous payment channel on the network during TON’s contest back in September. Here, we will be talking only about Fift (TON’s general-purpose programming language) and FunC (TON’s programming language for writing smart contracts).
The TON white paper provides more in-depth information about payment channels, but we will briefly explain them again.
Related: Behind the Scenes of TON: Lessons Learned on Deploying Smart Contracts, Part 1
A synchronous payment channel allows sending transactions between two users off-chain using on-chain assets. In our case — GRAMs. It is impossible for one party to cheat the other off-chain, and transactions are made much faster than executing layer-one blockchain transactions, as only user devices are used to complete them without having to write to the blockchain. There are two basic operations: deposit and withdraw. The withdrawal is the most challenging one to implement.
To make a correct withdrawal, users need to provide the latest information about the state of their channel. The state consists of the steps and digital signatures of each participant, which means it’s not possible to provide a correct state with data that has not been approved by both parties.
To deploy a smart contract, you need to write a deploy script in Fift and compile it to a .boc (bag of cells) file. Doing this makes multiple cells that will be linked to each other. GRAMs then need to be sent to the address that was received during deploy script execution. Once GRAMs are on the address, send the .boc file to the network and the contract will be deployed.
To make a function call, write a script that will send an external message to the deployed smart contract.
Basically, anything on TON is a cell with some references. A bag of cells is a data structure that was designed by the Telegram team. It is an actor model. More details are at TON whitepaper: “everything is a bag of cells.” You are building a cell that will interact with another cell when it is deployed.
Each peer-to-peer payment channel is a single smart contract. Let’s take a look at the segments of a smart contract.
Related: What to Expect From the Telegram Open Network: A Developer’s Perspective
A serialized Fift script is used to deploy a contract. It is saved to a .boc file and sent to the network via TON Cli, the network’s light client.
The latest cell on the stack is the result of executing the above Fift script.
The usual segments of a Fift deploy script include (but are not limited to):
- Code of the smart contract as a single cell (usually written in FunC, then compiled into Fift ASM code and included in the main .fif file using path-to-compiled-asm.fif).
- Initial storage of the smart contract (see below).
- New smart contract address (the hash from the initial state of the smart contract that also includes the smart contract code cell and the initial storage cell).
- Arguments of the first call of the recv_external function (the amount of arguments and type depends on the contract).
- An external message cell for initialization, which will be serialized into bytes and packed to the .boc file, which consists of all the data from points 1–4 and some additional ones that are still lacking documentation.
When the .boc is compiled, a specific amount of GRAMs need to be sent to the smart contract address. The .boc file must be sent to the network to initialize the smart contract. The amount of GRAMs depends on the size and volume of calculations of the deployed smart contract’s external message cell (not only the code of it). Gas × gas price is taken from the deployed smart contract balance. This amount is the minimum needed to pay for gas during the deployment.
A representation of the storage:
- seqno 32 bits
- contract_status 4 bits
- first_user_pubkey. The first party’s public key 256 bits
- second_user_pubkey. The second party’s public key 256 bits
- time_to_send. Time to send after the first actual state being submitted 32 bits (valid until 2038)
- depositSum. The deposited sum of two participants up to 121 bits
- state_num 64 bits. The current amount of states that occurred
A cell contains up to 1023 bits and four references to other cells. We were able to fit the entire storage onto one cell without a single reference. Our storage can take up a maximum of 765 bits.
All smart contract states
0x0 — Deployment state
0x1 — Channel opened and ready for deposit
0x2 — Deposit by user 1
0x3 — Deposit by user 2
0x4 — The deposit is blocked. It is possible to provide a state to the smart contract
0x5 — User 1 has provided the state
0x6 — User 2 has provided the state
0x7 — The channel is closed
The deposit function receives a message from a simple wallet (transfer) with an additional body payload.
Depositing GRAMs to the channel:
- The user generates an additional body payload that includes a message (for example, 1 bit) and its signature in a separate .fif file.
- Body payload is compiled to a .boc file.
- Body payload is loaded from this .boc file into a .fif file as a body-cell “transferring” reference (the .fif is responsible for transferring GRAMs from the wallet).
- The recv_external function is called with arguments (the deposit amount and the destination address of the channel) when the compiled .fif file is sent to the network.
- The send_raw_message function is executed. Deposited GRAMs and additional body payload is sent to a P2P channel smart contract destination address.
- The recv_internal function of the P2P channel smart contract is called. GRAMs are received by channel contracts.
The deposit function can be called if the state of the P2P channel smart contract is 0x1 or 0x2 or 0x3.
FunC code that checks the state:
Only the owners of the public keys (written in the initial storage) are allowed to make a deposit. The smart contract checks the signature of each internal message that will be received through the recv_internal function. If the message is signed by one of the public key owners, the contract status changes to 0x2 or 0x3 (0x2 if it is public key 1 and 0x3 if it is public key 2). If all users have made a deposit, the contract status changes to 0x4 on the same function call.
The FunC code responsible for changing contract status:
Funds can be returned if a counterparty has not made a deposit on time.
To do that, a user needs to provide their address and signature via external message. The funds will be refunded if the provided signature belongs to public key 1 or public key 2 (persons who made a deposit) and the contract status is 0x2 or 0x3.
FunC code that is responsible for verifying the refund application:
Each person should provide an exit state, the signature of this state, and signature of the body message.
- Smart contract address (to exclude the possibility of entering the correct state from the previous P2P channel with the same participants).
- The final balance of the first participant.
- The final balance of the second participant.
- State number.
The body message signature is stored in the main slice, the state is stored in a separate reference, and state signatures are stored as references to a “signatures” reference to avoid cell overflow.
Check the body message signature and determine the participant.
Check that it is the turn of the participant or 24 hours have passed since the last entered state. Write the turn of the current participant (0x5 or 0x6) to the contract status.
An example of a correct signature of the body message for the owner of first_user_pubkey:
We then need to verify that the smart contract address written to the state is the actual contract address:
Next, we need to verify signatures under the state:
After that, there are two assertions:
- The deposited amount from the storage should be equal to the sum of the total balances of the participants.
- The new entered state number must be greater than or equal to the previous one.
In case of new_state_num > state_num we need to store new_state_num with the new time_to_send equaling to now() + 86401 (24 hours from the current time), and also write the actual contract status (0x5 if first participant made a call, otherwise 0x6).
In another case, if new_state_num == state_num we need to put an additional two references to the “signatures” reference with addresses of each participant and signatures under their addresses.
If the signatures are correct, GRAMs are withdrawn from one address and put into the owner’s address.
Each time a successful call happens, we need to store all storage data even if it doesn’t change.
The assumption is that the first user deployed the contract and the participants agreed on commissions. The agreement on commissions in our case is reaching off-chain.
We have not yet figured out how to calculate the total commission, taking into account the fact that players can write an irrelevant state and record actual states after that. Keep in mind that we need to pay fees from the P2P channel smart contract each time we successfully call recv_internal or recv_external functions.
As mentioned earlier, we need to add some amount of GRAMs to a non-bounceable future smart contract address in order to initialize it.
On the last day of the competition, TON’s developers made a commit to the stdlib.fc library with a new function that allows getting the actual smart contract balance.
Suggestions for possible solutions to this problem are welcome!
FunC and Fift allow any developer access to the low-level world of software engineering, opening new opportunities and features for blockchain developers who have already gotten used to Ethereum or any other smart contract platform. It is important that TON is a sharded blockchain, so implementing smart contracts on it is more challenging. For example, Ethereum’s contracts run synchronously and do not require handling situations such as waiting for an answer from another contract.
The asynchronous way of smart contract communication is the only option to make it scalable, and TON has these options. Our solution ended up being more difficult to implement than Solidity, but there is always a trade-off. It is definitely possible to build an advanced smart contract on TON, and the way that TON’s team handled it is very impressive. We are looking forward to seeing more libraries and tools that will help to deploy and build FunC contracts.
We thoroughly enjoyed all the tasks and wish that we’d had more time to implement all of them. Nevertheless, we won two prizes at TON Contest: first place for best synchronous payment channel as well as third place for best asynchronous payment channel.
We will share our own personal feedback in part three.
The views, thoughts and opinions expressed here are the authors’ alone and do not necessarily reflect or represent the views and opinions of Cointelegraph.