Tezos (Part 1): Creating, Deploying, and Interacting with a Contract
Learn how to perform key operations on Tezos, including creating, testing, compiling, deploying, and interacting with smart contracts.
Getting started with Tezos | Part 1 | Part 2 | Part 3
In this blog post, we will demonstrate how to create, deploy, and interact with a contract on the Tezos blockchain. Specifically, we will develop a generic liquidity pool contract that will enable users to deposit and withdraw Tezos tokens (tez
) without any restriction. In our example, user Alice will deposit 10 tez
to the contract, and then user Bob will withdraw 5 tez
without making a prior deposit.
Note: We encourage developers to read the documentation of the LIGO language before proceeding with this tutorial.
Creating a contract
We are going to create a contract equivalent to this one written in Solidity. The final version of the contract can be found in this GitHub repo. The contract will be written in the LIGO language and then compiled to Michelson.
In order to compile and test the contract, we will need to install the LIGO CLI. You can check out the installation procedure in the official documentation. (Note: We encourage developers to use the vscode editor and the plugin developed by the LIGO team.)
Now, we can start to write the contract. We can create a folder and the file for our contract with the following commands. Note: open v1-public-pool.ligo
in your editor to start writing the contract.
$ mkdir tezos-defi && cd tezos-defi$ touch v1-public-pool.ligo
The current version of Tezos only supports a unique entry point for each contract (though this will change in the upcoming Babylon upgrade). The recommended workaround for this limitation is to use an argument, indicating the operation we want to perform.
The two operations we will exemplify in this tutorial are Deposit and Withdraw. In our contract, they will be represented with a variant type as shown below.
type entry_action is
| Deposit
| Withdraw
The storage for our contract will be a simple record with a liquidity field of the tez
type, representing the funds that our pool contains.
type finance_storage is record
liquidity: tez;
end
Note: At the time this tutorial was written, LIGO had an issue with tez
and mtz
getting mixed up. However, LIGO’s team is working on fixing it. In this guide, we use tez
for declarations and mtz
for literals.
Defining an entry point
The entry point for a contract in Tezos refers to the method selected to listen to external communication. It always receives two arguments: the parameters for the method and the contract storage. It also has a fixed signature for the response, operation list, and storage.
In the contract entry point, the parameter will be of the entry_action
type, which we already defined. The value for this parameter must be one of the values defined by either the Deposit operation or the Withdraw operation. The body of the method will be empty. The output will be delegated depending on the value of the operation. In both cases, the contract storage is propagated.
The Deposit operation
The Deposit operation will take the amount sent to the Tx. If the amount is 0, it should fail as shown in the code comments below. (As this tutorial was written, LIGO had an issue with zero-value deposits. The issue will be resolved in the next release, but that is why we are skipping the action if the condition is true. If the amount is bigger than 0, we will increase the liquidity.)
We will define the storage as var
(not const
), because we are going to modify the storage.
function depositImp(var finance_storage: finance_storage)
: (list(operation) * finance_storage) is
block {
if amount = 0mtz
then skip //fail(“No tez transferred!”);
else block {
finance_storage.liquidity := finance_storage.liquidity + amount;
}
} with(noOperations, finance_storage)
We will return noOperations
as the first component in the return value. We have to define this in the global scope of the contract. This defines a list initialized with nil
.
const noOperations: list(operation) = nil;
The Withdraw operation
The Withdraw operation allows a user to withdraw a fixed amount of tez
from the liquidity pool. The validation checks if the contract has enough tez
to be transferred to the sender. We are using the global “sender” from LIGO, which refers to the account that originated the transaction. If the validation succeeds, the amount of the liquidity pool contract will decrease, and a transaction to the sender will be created and returned as the first argument of the function result. If the transaction fails, the contract storage will be reverted, and the liquidity pool will remain unchanged.
Testing the contract
Now that we have the full contact functionality, we can proceed to test it part by part. Thanks to the functional approach of the Tezos blockchain, we can test smart contracts without deploying them to a network. By using the dry-run instruction that LIGO provides, we can run and analyze the result.
Let’s test a deposit by running the command below.
$ ligo dry-run v1-public-pool.ligo --syntax pascaligo main “Deposit(unit)” “record liquidity = 0mtz; end”> tuple[ list[]
record[
liquidity -> 0tz
]
]
Note that liquidity remains 0, because we haven’t sent any tez
to the transaction. If we replace the skip
with fail (“No tez transferred!”);
, it will return an error, and the transaction will be declined. To make it work we need to set the --amount
parameter to a value greater than 0.
$ ligo dry-run v1-public-pool.ligo --syntax pascaligo --amount 1.55 main “Deposit(unit)” “record liquidity = 0mtz; end”> tuple[ list[]
record[
liquidity -> 1550000tz
]
]
Note: 1 tez
is equal to 1,000,000 mtz
.
To test the Withdraw method, we can proceed just as before. This time, we will initialize the storage with tez
, and the liquidity pool will decrease.
$ ligo dry-run v1-public-pool.ligo --syntax pascaligo main “Withdraw(unit)” “record liquidity = 10000mtz; end”> tuple[ list[
Operation(…bytes)
]
record[
liquidity -> 9000tz
]
]
In both cases, the method response has the same structure: a list of operations and the storage.
Compiling the contract
Now, it’s time to deploy a smart contract to the Tezos Alphanet.
The first step is to use LIGO’s compile-contract
instruction as shown below.
# Compiling the contract and fixing line ending$ ligo compile-contract v1-public-pool.ligo main | tr -d ‘\r’ > v1-public-pool.tz
Next, we deploy our contract to provide the initial storage. However, it is not possible to do so using the LIGO syntax, so we will convert it to the Michelson language. Luckily, LIGO provides a command to compile it.
$ ligo compile-storage v1-public-pool.ligo main “record liquidity = 0mtz; end”
> 0
That’s all we need to deploy a contract.
Now that we have the contract deployed, we can copy it to a Docker container by running the following command.
$ docker cp tezos-defi/v1-public-pool.tz alphanet_node_1_15080fa44b96:/home/tezos
Note: Replace a container name with the one running in your machine, you can get it by running docker ps
.
Interacting with Tezos
In the previous blog post, we created two accounts on the Alphanet — test and test2 — using tezos-client
through a public node. Each time we wanted to run a command, we had to pass some arguments to configure the public node. This time, we will create an alias, so the following commands will be shorter.
To do so, enter the shell with the following command.
$ ./alphaclient.sh shell
From now on, we will be switch between ./alphaclient.sh shell
and /tezos-defi working directory
. As a general rule, all the commands related to tezos-client
need to be performed inside ./alphaclient.sh shell
, and all the other commands need to be run from the workspace. To better illustrate this, we will put a comment at the top of each code sample.
To continue with this tutorial, please read our previous blog post on Tezos basics and then run ./alphaclient.sh shell
.
The first step is to create an alias for tezos-client
with our network configuration. The arguments are:
-A
: indicates the node’s address-P
: indicates the node’s RPC port-w
: specifies how many confirmation blocks the client has to wait before considering an operation as included
# ./alphaclient.sh shell$ alpha-client gen keys contractOwner$ alpha-client gen keys bob
Both accounts will contain 0 tez
. Let’s change that by transferring tez
from test2.
# ./alphaclient.sh shellalpha-client transfer 10500 from test2 to contractOwner --burn-cap 0.257alpha-client transfer 5 from test2 to bob --burn-cap 0.257
If you do not specify the amount of tez
available to be burned as a fee, it will fail with a message as shown below.
Fatal error:
The operation will burn ꜩ2.281 which is higher than the configured burn cap (ꜩ0).
Use ` --burn-cap 2.281` to emit this operation.
Deploying the contract
The deployment of a Tezos smart contract is called “origination.” It represents the creation of an account, that has a certain script attached to the smart contract. Contracts created through originations have an address starting with KT1…
(originated accounts) as opposed to implicit accounts with addresses starting with tz1…
(also tz2
or tz3
, depending on the elliptic curve used).
To deploy the contract, we have to run the following command.
# ./alphaclient.sh shell$ alpha-client originate contract v1-public-pool for contractOwner transferring 0 from contractOwner running v1-public-pool.tz --init 0 --burn-cap 2.314
Note: If you want to try a mock operation, you can add the--dry-run
parameter at the end of the instruction. This operation will print important information, like the transaction ID, the amount of tez
the deployment cost was worth, the address of the new contract, etc. We encourage you to take a closer look at it.
tezos-client
provides us with a command to list the address of the contracts deployed.
# ./alphaclient.sh shell$ alpha-client list known contracts
> contract: KT1HkL7uH53X12CZeH8Jv9H2AYHrA9M9xDUt
Contract memorized as v1-public-pool.
Interacting with the contract
In order to make the contract functional, we need to feed the contract with tez
. So, using the contractOwner
account, we will deposit 10,000 tez
.
The first step is to obtain the parameter that will be sent to the contract’s entry point. We can get the Michelson code with the command below.
# /tezos-defi working directory$ ligo compile-parameter v1-public-pool.ligo -s pascaligo main “Deposit(unit)”
> (Left Unit)
Now, we can call the contract.
# ./alphaclient.sh shell$ alpha-client transfer 10000 from contractOwner to v1-public-pool
--arg “(Left Unit)” --burn-cap 0.004
Next, we can verify the result in several ways. We can query the contract storage.
# ./alphaclient.sh shell$ alpha-client get script storage for v1-public-pool
> 10000000000
We can also verify that the contractOwner
wallet has decreased by 10,000 tez
.
# ./alphaclient.sh shell$ alpha-client get balance for contractOwner
> 8044.90585 ꜩ
Or we can use a block explorer.
Now, we can try the Withdraw operation. This time the bob account will withdraw some tez
from our liquidity pool. Remember that we have transferred 5 tez
to bob, this will be more than enough to pay transaction fees.
To obtain the Michelson instruction, we need to run the following command.
Now, we call the Withdraw contract method.
# ./alphaclient.sh shell$ alpha-client get balance for bob$ alpha-client transfer 0 from bob to v1-public-pool --arg “(Right Unit)”$ alpha-client get balance for bob
Summary
Now that we’ve learned how to set up and configure tezos-client
and deploy a smart contract, the next step is to add some complexity to the contract. In the upcoming blog post, we will restrict the Withdraw operation for only those accounts that have enough deposited tez
.