Tezos (Part 2): A private liquidity pool
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.