Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial stress test for liquidation auction #159

Merged
merged 12 commits into from
Jun 21, 2021
7 changes: 6 additions & 1 deletion default.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ doCheck ? false }:
{ doCheck ? false, e2eTestsHack ? false }:
utdemir marked this conversation as resolved.
Show resolved Hide resolved

let
sources = import ./nix/sources.nix { };
Expand Down Expand Up @@ -69,6 +69,11 @@ rec
name = "checker-michelson";
buildInputs = [ ligoBinary ] ++ (with pkgs; [ ruby ]) ++ ocamlDeps pkgs;
src = checkerSource;
patchPhase = pkgs.lib.optional e2eTestsHack ''
set -x
cat ${./patches/e2e-tests-hack.patch} | patch -p1
set +x
'';
buildPhase = ''
export HOME=$(mktemp -d)
make build-ligo
Expand Down
155 changes: 153 additions & 2 deletions e2e/main.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import os
import json
import time
from collections import OrderedDict
import unittest
from datetime import datetime
Expand All @@ -9,6 +10,9 @@
from pytezos.operation import MAX_OPERATIONS_TTL

PROJECT_ROOT = os.path.join(os.path.dirname(__file__), "../")
CHECKER_DIR = os.environ.get(
"CHECKER_DIR", os.path.join(PROJECT_ROOT, "generated/michelson")
)

from checker_client.checker import *

Expand Down Expand Up @@ -60,7 +64,7 @@ def test_e2e(self):
print("Deploying Checker.")
checker = deploy_checker(
self.client,
checker_dir=os.path.join(PROJECT_ROOT, "generated/michelson"),
checker_dir=CHECKER_DIR,
oracle=oracle.context.address,
ctez=ctez["fa12_ctez"].context.address,
ttl=MAX_OPERATIONS_TTL,
Expand Down Expand Up @@ -157,5 +161,152 @@ def call_checker_endpoint(name, param, amount=0):
print(json.dumps(gas_costs_sorted, indent=4))


class LiquidationsStressTest(SandboxedTestCase):
def test_liquidations(self):
print("Deploying the mock oracle.")
oracle = deploy_contract(
self.client,
source_file=os.path.join(PROJECT_ROOT, "util/mock_oracle.tz"),
initial_storage=(self.client.key.public_key_hash(), 1000000),
ttl=MAX_OPERATIONS_TTL,
)

print("Deploying ctez contract.")
ctez = deploy_ctez(
self.client,
ctez_dir=os.path.join(PROJECT_ROOT, "vendor/ctez"),
ttl=MAX_OPERATIONS_TTL,
)

print("Deploying Checker.")
checker = deploy_checker(
self.client,
checker_dir=CHECKER_DIR,
oracle=oracle.context.address,
ctez=ctez["fa12_ctez"].context.address,
ttl=MAX_OPERATIONS_TTL,
)

print("Deployment finished.")

def call_endpoint(contract, name, param, amount=0):
print("Calling", contract.key.public_key_hash(), "/", name, "with", param)
return inject(
self.client,
getattr(contract, name)(param)
.with_amount(amount)
.as_transaction()
.autofill(ttl=MAX_OPERATIONS_TTL)
.sign(),
)

def call_bulk(bulks, *, chunk_size):
utdemir marked this conversation as resolved.
Show resolved Hide resolved
chunks = [
bulks[i : i + chunk_size] for i in range(0, len(bulks), chunk_size)
]
for chunk_no, chunk in enumerate(chunks, 1):
utdemir marked this conversation as resolved.
Show resolved Hide resolved
print(
"Sending",
len(bulks),
"operations as bulk:",
"Chunk",
chunk_no,
"of",
len(chunks),
)
inject(
self.client,
self.client.bulk(*chunk).autofill(ttl=MAX_OPERATIONS_TTL).sign(),
)

call_bulk(
[
checker.create_burrow((0, None)).with_amount(200_000_000),
checker.mint_kit((0, 80_000_000)),
],
chunk_size=10,
)

call_endpoint(
ctez["ctez"], "create", (1, None, {"any": None}), amount=2_000_000
)
call_endpoint(ctez["ctez"], "mint_or_burn", (1, 100_000))
call_endpoint(ctez["fa12_ctez"], "approve", (checker.context.address, 100_000))

call_endpoint(
checker,
"add_liquidity",
(100_000, 100_000, 5, int(datetime.now().timestamp()) + 20),
)

burrows = list(range(1, 1001))

call_bulk(
[
checker.create_burrow((burrow_id, None)).with_amount(100_000_000)
for burrow_id in burrows
],
chunk_size=100,
)

# Mint as much as possible from the burrows. All should be identical, so we just query the
# first burrow and mint that much kit from all of them.
max_mintable_kit = checker.metadata.burrowMaxMintableKit(
(self.client.key.public_key_hash(), 1)
).storage_view()

bulks = []
for burrow_no in burrows:
bulks.append(
checker.touch_burrow((self.client.key.public_key_hash(), burrow_no))
)
utdemir marked this conversation as resolved.
Show resolved Hide resolved
bulks.append(checker.mint_kit(burrow_no, max_mintable_kit))

call_bulk(bulks, chunk_size=100)

# Change the index (kits are 100x valuable)
#
# Keep in mind that we're using a patched checker on tests where the protected index
# is much faster to update.
call_endpoint(oracle, "update", 100_000_000)

# Oracle updates lag one touch on checker
call_endpoint(checker, "touch", None)
call_endpoint(checker, "touch", None)
time.sleep(20)
call_endpoint(checker, "touch", None)

# Now burrows should be overburrowed, so we liquidate them all.
#
# This should use the push_back method of the AVL tree.
bulks = []
for burrow_no in burrows:
bid = (self.client.key.public_key_hash(), burrow_no)
bulks.append(checker.touch_burrow(bid))
bulks.append(checker.mark_for_liquidation(bid))
utdemir marked this conversation as resolved.
Show resolved Hide resolved

call_bulk(bulks, chunk_size=40)

# This touch starts a liquidation auction
#
# This would use the split method of the AVL tree. However, if the entire queue
# has less tez than the limit, the 'split' method would trivially return the entire
# tree without much effort; so here we should ensure that the liquidation queue has
# more tez than the limit (FIXME).
call_endpoint(checker, "touch", None)

# And we place a bid

ret = checker.metadata.currentLiquidationAuctionMinimumBid().storage_view()
auction_id, minimum_bid = ret["contents"], ret["nat_1"]
# The return value is supposed to be annotated as "auction_id" and "minimum_bid", I
# do not know why we get these names. I think there is an underlying pytezos bug
# that we should reproduce and create a bug upstream.
Comment on lines +300 to +302
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, that's weird indeed 🤔 Not sure if it's a pytezos bug; it reminds me of #81.


call_endpoint(
checker, "liquidation_auction_place_bid", (auction_id, minimum_bid)
)


if __name__ == "__main__":
unittest.main()
12 changes: 12 additions & 0 deletions patches/e2e-tests-hack.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
diff --git a/src/parameters.ml b/src/parameters.ml
index 395ecd1..4907c90 100644
--- a/src/parameters.ml
+++ b/src/parameters.ml
@@ -211,6 +211,7 @@ let[@inline] compute_current_burrow_fee_index (last_burrow_fee_index: fixedpoint
*)
let[@inline] compute_current_protected_index (last_protected_index: Ligo.tez) (current_index: Ligo.tez) (duration_in_seconds: Ligo.int) : Ligo.tez =
assert (Ligo.gt_tez_tez last_protected_index (Ligo.tez_from_literal "0mutez"));
+ let duration_in_seconds = Ligo.mul_int_int duration_in_seconds (Ligo.int_from_literal "1000") in
let last_protected_index = tez_to_mutez last_protected_index in
fraction_to_tez_floor
(clamp_int
13 changes: 12 additions & 1 deletion src/checker.ml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ open Error
open Fa12Interface
open Fa2Interface
open Mem
open Ptr

(* BEGIN_OCAML *)
let assert_checker_invariants (state: checker) : unit =
Expand Down Expand Up @@ -615,12 +616,15 @@ let entrypoint_remove_liquidity (state, p: checker * (lqt * ctez * kit * Ligo.ti
(** LIQUIDATION AUCTIONS *)
(* ************************************************************************* *)

let entrypoint_liquidation_auction_place_bid (state, kit: checker * kit) : LigoOp.operation list * checker =
let entrypoint_liquidation_auction_place_bid (state, (auction_id, kit): checker * (Ligo.nat * kit)) : LigoOp.operation list * checker =
assert_checker_invariants state;
let _ = ensure_no_tez_given () in

let bid = { address=(!Ligo.Tezos.sender); kit=kit; } in
let current_auction = liquidation_auction_get_current_auction state.liquidation_auctions in
let () = if nat_of_ptr (ptr_of_avl_ptr current_auction.contents) = auction_id
utdemir marked this conversation as resolved.
Show resolved Hide resolved
then ()
else Ligo.failwith error_InvalidLiquidationAuction in
utdemir marked this conversation as resolved.
Show resolved Hide resolved

let (new_current_auction, old_winning_bid) = liquidation_auction_place_bid current_auction bid in

Expand Down Expand Up @@ -920,6 +924,13 @@ let view_is_burrow_liquidatable (burrow_id, state: burrow_id * checker) : bool =
let burrow = burrow_touch state.parameters burrow in
burrow_is_liquidatable state.parameters burrow

let view_current_liquidation_auction_minimum_bid ((), state: unit * checker) : view_current_liquidation_auction_minimum_bid_result =
assert_checker_invariants state;
let auction = liquidation_auction_get_current_auction state.liquidation_auctions in
{ auction_id = nat_of_ptr (ptr_of_avl_ptr auction.contents)
; minimum_bid = liquidation_auction_current_auction_minimum_bid auction
}

(* ************************************************************************* *)
(** FA2_VIEWS *)
(* ************************************************************************* *)
Expand Down
5 changes: 4 additions & 1 deletion src/checker.mli
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,10 @@ val entrypoint_remove_liquidity : checker * (lqt * ctez * kit * Ligo.timestamp)
auction, or if the bid is too low.

Parameters:
- identifier of the current liquidation auction
- The amount of kit to be bid
*)
val entrypoint_liquidation_auction_place_bid : checker * kit -> LigoOp.operation list * checker
val entrypoint_liquidation_auction_place_bid : checker * (Ligo.nat * kit) -> LigoOp.operation list * checker

(** Claim the rewards of a completed liquidation auction. Fails if the sender
is not the auction winner, if the auction is still ongoing, or if the
Expand Down Expand Up @@ -257,6 +258,8 @@ val view_add_liquidity_min_lqt_minted : (ctez * checker) -> lqt
val view_remove_liquidity_min_ctez_withdrawn : (lqt * checker) -> ctez
val view_remove_liquidity_min_kit_withdrawn : (lqt * checker) -> kit

val view_current_liquidation_auction_minimum_bid: (unit * checker) -> view_current_liquidation_auction_minimum_bid_result

val view_burrow_max_mintable_kit : (burrow_id * checker) -> kit
val view_is_burrow_overburrowed : (burrow_id * checker) -> bool
val view_is_burrow_liquidatable : (burrow_id * checker) -> bool
Expand Down
6 changes: 6 additions & 0 deletions src/checkerTypes.ml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
open Kit
open Burrow
open CfmmTypes
open Parameters
Expand Down Expand Up @@ -45,3 +46,8 @@ type wrapper =
; metadata: (string, Ligo.bytes) Ligo.big_map
; deployment_state : deployment_state
}

type view_current_liquidation_auction_minimum_bid_result =
{ auction_id: Ligo.nat
; minimum_bid: kit
}
1 change: 1 addition & 0 deletions src/error.ml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ let[@inline] error_NotACompletedSlice : Ligo.int =
let[@inline] error_InvalidAvlPtr : Ligo.int = Ligo.int_from_literal "88"
let[@inline] error_InvalidLeafPtr : Ligo.int = Ligo.int_from_literal "89"
let[@inline] error_BurrowAlreadyExists : Ligo.int = Ligo.int_from_literal "90"
let[@inline] error_InvalidLiquidationAuction : Ligo.int = Ligo.int_from_literal "91"

let[@inline] error_GetContractOptFailure : Ligo.int = Ligo.int_from_literal "95"

Expand Down
4 changes: 4 additions & 0 deletions src/ligo.ml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ let int_from_literal s =
with exc -> failwith ("Ligo.int_from_literal: " ^ Printexc.to_string exc)

let int_from_int64 = Z.of_int64
let nat_from_int64 (t: Int64.t) =
let r = Z.of_int64 t in
assert (r > Z.zero);
gkaracha marked this conversation as resolved.
Show resolved Hide resolved
r

let add_int_int = Z.add

Expand Down
1 change: 1 addition & 0 deletions src/ligo.mli
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ val key_hash_from_literal : String.t -> key_hash (* IN LIGO: type-annotate with
val bytes_from_literal : String.t -> bytes (* IN LIGO: replace the double quotes with parens *)

val int_from_int64: Int64.t -> int (* NON-LIGO, temporary*)
utdemir marked this conversation as resolved.
Show resolved Hide resolved
val nat_from_int64: Int64.t -> nat (* NON-LIGO, temporary*)
val timestamp_from_seconds_literal : Int.t -> timestamp (* NON-LIGO: in LIGO they come from strings, or Tezos.now *)

(* OPERATIONS ON int *)
Expand Down
13 changes: 7 additions & 6 deletions src/ptr.ml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
type ptr = Ligo.int
type ptr = Ligo.nat
(* BEGIN_OCAML *) [@@deriving show] (* END_OCAML *)

let[@inline] ptr_null = Ligo.int_from_literal "0"
let[@inline] ptr_init = Ligo.int_from_literal "1"
let[@inline] ptr_next (t: ptr) = Ligo.add_int_int t (Ligo.int_from_literal "1")
let[@inline] nat_of_ptr (t: ptr) = t
let[@inline] ptr_null = Ligo.nat_from_literal "0n"
let[@inline] ptr_init = Ligo.nat_from_literal "1n"
let[@inline] ptr_next (t: ptr) = Ligo.add_nat_nat t (Ligo.nat_from_literal "1n")

(* BEGIN_OCAML *)
let compare_ptr = Common.compare_int
let random_ptr () = Ligo.int_from_int64 (Random.int64 Int64.max_int)
let compare_ptr = Common.compare_nat
let random_ptr () = Ligo.nat_from_int64 (Random.int64 Int64.max_int)
(* END_OCAML *)
1 change: 1 addition & 0 deletions src/ptr.mli
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
type ptr (* George: perhaps I'd prefer a phantom parameter to not mix up pointers. *)

val nat_of_ptr : ptr -> Ligo.nat
val ptr_null : ptr
val ptr_init : ptr
val ptr_next : ptr -> ptr
Expand Down
10 changes: 3 additions & 7 deletions tests/testChecker.ml
Original file line number Diff line number Diff line change
Expand Up @@ -895,12 +895,7 @@ let suite =
Ligo.Tezos.new_transaction ~seconds_passed:(5*60) ~blocks_passed:5 ~sender:bob_addr ~amount:(Ligo.tez_from_literal "0mutez");
assert_raises
(Failure (Ligo.string_of_int error_NoOpenAuction))
(fun () ->
Checker.entrypoint_liquidation_auction_place_bid
( checker
, (kit_of_mukit (Ligo.nat_from_literal "1_000n"))
)
);
(fun () ->Checker.view_current_liquidation_auction_minimum_bid ((), checker));
utdemir marked this conversation as resolved.
Show resolved Hide resolved

let kit_before_reward = get_balance_of checker bob_addr kit_token_id in
let _, checker = Checker.touch_with_index checker (Ligo.tez_from_literal "1_200_000mutez") in
Expand All @@ -922,11 +917,12 @@ let suite =
let kit_after_reward = get_balance_of checker alice_addr kit_token_id in

let touch_reward = Ligo.sub_nat_nat kit_after_reward kit_before_reward in
let auction_id = (Checker.view_current_liquidation_auction_minimum_bid ((), checker)).auction_id in

let (ops, checker) =
Checker.entrypoint_liquidation_auction_place_bid
( checker
, (kit_of_mukit (Ligo.nat_from_literal "4_200_000n"))
, (auction_id, kit_of_mukit (Ligo.nat_from_literal "4_200_000n"))
) in

let auction_id =
Expand Down