Tezos (Part 2): A private liquidity pool

Protofire.io
Protofire Blog
Published in
8 min readOct 24, 2019

Learn how to modify a contract to allow withdrawals only from the addresses that have previously made deposits.

Getting started with Tezos | Part 1 | Part 2 | Part 3

In this blog post, we will modify the v1-public-pool liquidity pool contract on Tezos. More specifically, we will create a private liquidity pool contract. It will only allow addresses that have previously deposited tez into the contract to withdraw the same amount.

In this tutorial, the Withdraw operation will not have an argument. Thus, only the amount deposited may be withdrawn. The final version of this contract can be found in this GitHub repo.

Note: We encourage developers to read our previous blog post, where we explain how to create and interact with the contract, before proceeding with this tutorial.

Modifying a contract

Let’s start by navigating to the tezos-defi folder and creating a new file for this private liquidity pool contract. (Note: Open v2-private-pool.ligo in your editor to start writing the contract.)

$ cd tezos-defi
$ touch v2-private-pool.ligo

The variant type for the contract entry point remains unchanged from Part 1 of this series.

type entry_action is
| Deposit
| Withdraw

In the contract’s storage, we need to add the map attribute, which indicates the address and the amount of tez controlled by that address.

type finance_storage is record 
deposits: map(address, tez);
liquidity: tez;
end

The main function representing the entry point remains unchanged from Part 1 of this series.

function main(const action: entry_action; var finance_storage: finance_storage): (list(operation) * finance_storage) is
block {
skip
} with case action of
| Deposit(param) -> depositImp(finance_storage)
| Withdraw(param) -> withdrawImp(finance_storage)
end;

The const action remains unchanged, but it needs to be added in the contract’s global scope.

const noOperations: list(operation) = nil;

The Deposit operation

The Deposit operation will require some modifications. First, we need to obtain the sender’s information, which can be found using the map attribute from the storage.

Once we have the sender’s information, we can validate if the sender has already made a deposit into the liquidity pool. If a deposit was made previously, we trigger an error. If the sender has not made a deposit yet, we take the amount of tez transferred, as well as update the map attribute and increase the size of the liquidity pool.

Note: The Deposit operation can be performed from the same address multiple times. However, the address needs to perform the Withdraw operation first before proceeding with depositing.

The Withdraw operation

In the Withdraw operation, we need to make some modifications to withdraw only the amount deposited. First, we will use the get_force function of the LIGO language. This function tries to obtain the key from the map attribute with the same address as the sender’s, if it is not found, it will trigger an error.

If the get_force function succeeds, we will check if the sender still has tez in the liquidity pool. If the pool is empty, the contract triggers a failure. If the pool has tez, the Withdraw operation is initiated for the total amount deposited and is added to the list of operations that the function will return. The size of the liquidity pool is then decreased.

Note: We are using the senderAddress constant, and we assign it with the sender’s value.

Testing the contract

Now that we have the full contact functionality, we can test it part by part.

Thanks to the functional approach of Tezos, we can test 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 with the dry-run. We are sending 73 tez to the contract’s main entry point using the Deposit operation. The storage is initialized with liquidity equaling zero and an empty map attribute.

# /tezos-defi working directory$ ligo dry-run v2-private-pool.ligo — syntax pascaligo — amount 73 main “Deposit(unit)” “record deposits = (map end: map(address, tez)); liquidity = 0mtz; end”> tuple[
list[]
record[
liquidity -> 73000000tz
deposits -> map[
@”tz1YaMudpVu13Z8ySeMQ9no1vGiTkLok9yzN” -> 73000000tz
]
]
]

The result was as expected, the pool’s liquidity was updated, and so was the map’s. Now, map contains an entry with the key equaling the sender’s address and the value equaling 73 tez.

Note: The sender address is a mock address provided by LIGO.

To test the Withdraw method, we can proceed similarly as above. First, we need to place the following line in the comments.

const senderAddress: address = sender;

And then remove the line below from the comments.

//const senderAddress: address =(“tz1MZ4GPjAA2gZxKTozJt8Cu5Gvu6WU2ikZ4” : address);

With the changes above, we are setting the sender to a fixed address. Then, when we pass the storage to ligo dry-run, we will initialize the storage with map that has an entry for the fixed address as the key and 1 tez as its value. The same happens for the liquidity attribute of the contract’s storage.

After running the test, we can see how the amount of the liquidity pool and the map value for the address are decreased to 0 tez.

# tezos-defi working directory$ ligo dry-run v2-private-pool.ligo — syntax pascaligo — amount 1 main “Withdraw(unit)” ‘record deposits = map (“tz1MZ4GPjAA2gZxKTozJt8Cu5Gvu6WU2ikZ4” : address) -> 1000000mtz end; liquidity = 1000000mtz; end’
> tuple[
list[
Operation(…bytes)
]
record[
liquidity -> 0tz
deposits -> map[
@”tz1MZ4GPjAA2gZxKTozJt8Cu5Gvu6WU2ikZ4" -> 0tz
]
]
]

Compiling and deploying the contract

Now, it’s time to compile the contract. For this purpose, we will use the same instructions as in Part 1 of this series.

# /tezos-defi working directory# compile
$ ligo compile-contract v2-private-pool.ligo main | tr -d ‘\r’ > v2-private-pool.tz
# compile storage
$ ligo compile-storage v2-private-pool.ligo main “record deposits = (map end: map(address, tez)); liquidity = 0mtz; end”
> (Pair {} 0)
# move the compiled contract to docker container$ docker cp v2-private-pool.tz alphanet_node_1_6a397f00c2b9:/home/tezos

In a previous blog post, we created some wallets. Let’s inspect their balance by running the following command.

# ./alphaclient.sh shell$ alias alpha-client=”tezos-client -A rpcalpha.tzbeta.net -P 443 -S -w none”$ alpha-client get balance for contractOwner
> 21931.781292 ꜩ
$ alpha-client get balance for bob
> 3 ꜩ

To deploy a contract, we will use the same commands as in Part 1 of this series. Note that after originating the contract, the response will contain the address and alias for the new contract.

# ./alphaclient.sh shell$ alpha-client originate contract v2-private-pool for contractOwner transferring 0 from contractOwner running v2-private-pool.tz — init “(Pair {} 0)” — burn-cap 4.442> New contract KT1C93MxGVubAAVA8JdDYzprMfLKVn2kq1oN originated.Contract memorized as v2-private-pool.

Interacting with the contract

The first thing to do is to inspect the contract’s storage.

# ./alphaclient.sh shell$ alpha-client get script storage for v2-private-pool
> Pair {} 0

Next, we recap the Michelson instructions to call the entry point.

# /tezos-defi working directory$ ligo compile-parameter v2-private-pool.ligo -s pascaligo main “Deposit(unit)”
> (Left Unit)
$ ligo compile-parameter v2-private-pool.ligo -s pascaligo main “Withdraw(unit)”
> (Right Unit)

Let’s make the first contract call, which will try to withdraw tez from our contract to bob’s wallet This should fail, since bob didn’t make a deposit.

# ./alphaclient.sh shell$ alpha-client transfer 0 from bob to v2-private-pool — arg “(Right Unit)”
> Runtime error in contract KT1DJtEeE7cMJ2PHn717N4gVYQhBsM9GmLCM:

script reached FAILWITH instruction
with “GET_FORCE”

This time the call will fail with the GET_FORCE error. It indicates that there is no key in the map attribute for the sender’s address, which makes sense, since bob didn’t make a deposit to the liquidity pool.

Let’s continue by making a deposit from the bob’s address.

# ./alphaclient.sh shell$ alpha-client transfer 2 from bob to v2-private-pool — arg “(Left Unit)” — burn-cap 0.037> Transaction:
Amount: ꜩ2
From: tz1dn56b2qshAAhbwyQuSWoyiaqyV5PYXidt
To: KT1DJtEeE7cMJ2PHn717N4gVYQhBsM9GmLCM
Parameter: (Left Unit)
This transaction was successfully applied
Updated storage:
(Pair { Elt 0x0000c6f7995d920ef51cc2977e6865695ec8fd47a67d 2000000 } 2000000)

The call was executed correctly, and the contract storage was updated. We can revalidate it by consulting the contract storage again.

# ./alphaclient.sh shell$ alpha-client get script storage for v2-private-pool> Pair { Elt “tz1dn56b2qshAAhbwyQuSWoyiaqyV5PYXidt” 2000000 } 2000000

We can check that the contract storage has an entry for the bob’s address of 2 tez. We can also check that the balance of the bob’s address was reduced by 2 tez.

Note: There may be a delay on balance updates after a transaction.

# ./alphaclient.sh shell$ alpha-client get balance for bob> 0.952667 ꜩ

Now, we can retry the Withdraw call again. This time it should succeed, because the contract has a deposit from bob.

# ./alphaclient.sh shell$ alpha-client transfer 0 from bob to v2-private-pool — arg “(Right Unit)”
> Transaction:
Amount: ꜩ0
From: tz1dn56b2qshAAhbwyQuSWoyiaqyV5PYXidt
To: KT1DJtEeE7cMJ2PHn717N4gVYQhBsM9GmLCM
Parameter: (Right Unit)
This transaction was successfully applied
Updated storage:
(Pair {} 0)

Note: The contract storage was updated. Now, map does not have an entry for bob. The balance of the bob’s address was increased by 2 tez.

# ./alphaclient.sh shell$ alpha-client get balance for bob
> 2.94133 ꜩ

Summary

We have updated a liquidity pool contract with more restrictive functionality. Only addresses that have deposited tez may make a withdrawal.

We encourage you to try more contract calls. For example, you could try to withdraw again using the bob’s alias. In this case, map will have a key for bob, but it will have 0 tez deposited. The contract will respond differently than it did the first time.

In our next blog post, we will move on to the next level by further modifying our contract to handle interest. The amount of tez a user will be able to withdraw after a deposit will increase depending on how many blocks have passed between a deposit and withdrawal.

--

--

Protofire.io
Protofire Blog

We help token-based startups with protocol & smart contract engineering, high-performance trusted data feeds (oracles), and awesome developer tools (SDKs/APIs).