diff --git a/.ligoproject b/.ligoproject new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/.ligoproject @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/README.md b/README.md index 986184c4..674d10ba 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,13 @@ Each time the CFMM pushes its rate to the ctez contract, the drift, and the targ If the price of ctez implied by the CFMM is below the target, the drift is *raised* by `max(1024 * (target / price - 1)^2, 1) * 2^(-48)` times the number of seconds since the last adjustment. If it is below, it is *lowered* by that amount. This corresponds roughly to a maximum adjustment of the annualized drift of one percentage point for every fractional day since the last adjustment. The adjustment saturates when the discrepancy exceeds one 32ndth. Note that, by a small miracle, `ln(1.01) / year / day ~ 1.027 * 2^(-48) / second^2` which we use to simplify the computation in the implementation. +### Curve + +If `x` is the quantity of tez, and `y` the quantity of ctez, and `t` the target (in tez per ctez), then CFMM uses the constant formula `(x + y)^8 - (x - y)^8 = k`. The price is equal to the target when `y = x / t` and, on that point, all derivatives from the 2nd to the 7th vanish, meaning there is more liquidity there. +To give an example, if `t = 1`, `x = 100` and `y = 100`, and a user adds `dx = 63` to the pool, they receive `dy = 62.4`, that is less than `1%` slippage for taking nearly two thirds of the `y` pool! + +The target is fed to the CFMM by the ctez contract. + ## Rationale If the price of ctez remains below its target, the drift will keep increasing and at some point, under a quadratically compounding rate vaults are forced into liquidation which may cause ctez to be bid up to claim the tez in the vaults. diff --git a/TODO b/TODO new file mode 100644 index 00000000..9474c35b --- /dev/null +++ b/TODO @@ -0,0 +1,5 @@ +- check for bugs +- ensure last price of block is used or perhaps VWAP +- negative fees when restoring AMM towards target +- ensure drift is updated based on the price before the last update, not the last update +- lock LPs for a few blocks after they initiate withdrawal request diff --git a/attic/cfmm_tez_ctez.old.preprocessed.mligo b/attic/cfmm_tez_ctez.old.preprocessed.mligo new file mode 100644 index 00000000..372e1da2 --- /dev/null +++ b/attic/cfmm_tez_ctez.old.preprocessed.mligo @@ -0,0 +1,568 @@ +[@inline] let const_fee = 9995n (* 0.05% fee *) +[@inline] let const_fee_denom = 10000n + +(* Pick one of CASH_IS_TEZ, CASH_IS_FA2, CASH_IS_FA12. tokenToToken isn't supported for CASH_IS_FA12 *) +//#define CASH_IS_TEZ +//#define CASH_IS_FA2 +//#define CASH_IS_FA12 + +(* If the token uses the fa2 standard *) +//#define TOKEN_IS_FA2 +(* To support baking *) +//#define HAS_BAKER +(* To push prices to some consumer contract once per block *) +//#define ORACLE + +(* ============================================================================ + * Entrypoints + * ============================================================================ *) + +type add_liquidity = + [@layout:comb] + { owner : address ; (* address that will own the minted lqt *) + minLqtMinted : nat ; (* minimum number of lqt that must be minter *) + maxTokensDeposited : nat ; (* maximum number of tokens that may be deposited *) + + deadline : timestamp ; (* time before which the request must be completed *) + } + +type remove_liquidity = + [@layout:comb] + { [@annot:to] to_ : address ; (* recipient of the liquidity redemption *) + lqtBurned : nat ; (* amount of lqt owned by sender to burn *) + minCashWithdrawn : nat ; (* minimum amount of cash to withdraw *) + minTokensWithdrawn : nat ; (* minimum amount of tokens to withdraw *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +type cash_to_token = + [@layout:comb] + { [@annot:to] to_ : address ; (* where to send the tokens *) + minTokensBought : nat ; (* minimum amount of tokens that must be bought *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +type token_to_cash = + [@layout:comb] + { [@annot:to] to_ : address ; (* where to send the cash *) + tokensSold : nat ; (* how many tokens are being sold *) + minCashBought : nat ; (* minimum amount of cash desired *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +type token_to_token = + [@layout:comb] + { outputCfmmContract : address ; (* other cfmm contract *) + minTokensBought : nat ; (* minimum amount of tokens bought *) + [@annot:to] to_ : address ; (* where to send the output tokens *) + tokensSold : nat ; (* amount of tokens to sell *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +(* getbalance update types for fa12 and fa2 *) +type update_fa12_pool = nat +type update_fa2_pool = ((address * nat) * nat) list + +type update_token_pool_internal = update_fa12_pool + +type entrypoint = +| AddLiquidity of add_liquidity +| RemoveLiquidity of remove_liquidity +| CashToToken of cash_to_token +| TokenToCash of token_to_cash +| TokenToToken of token_to_token +| UpdatePools of unit +| UpdateTokenPoolInternal of update_token_pool_internal +| SetLqtAddress of address + +(* ============================================================================= + * Storage + * ============================================================================= *) + +type storage = + [@layout:comb] + { tokenPool : nat ; + cashPool : nat ; + lqtTotal : nat ; + pendingPoolUpdates : nat ; + tokenAddress : address ; + lqtAddress : address ; + lastOracleUpdate : timestamp ; + consumerEntrypoint : address ; + } + +(* Type Synonyms *) + +type result = operation list * storage + +(* FA2 *) +type token_id = nat +type balance_of = ((address * token_id) list * ((((address * nat) * nat) list) contract)) +(* FA1.2 *) +type get_balance = address * (nat contract) + +(* FA1.2 *) +type token_contract_transfer = address * (address * nat) + +(* FA12 *) +type cash_contract_transfer = address * (address * nat) + +(* custom entrypoint for LQT FA1.2 *) +type mintOrBurn = + [@layout:comb] + { quantity : int ; + target : address } + +(* ============================================================================= + * Error codes + * ============================================================================= *) + +[@inline] let error_TOKEN_CONTRACT_MUST_HAVE_A_TRANSFER_ENTRYPOINT = 0n +[@inline] let error_ASSERTION_VIOLATED_CASH_BOUGHT_SHOULD_BE_LESS_THAN_CASHPOOL = 1n +[@inline] let error_PENDING_POOL_UPDATES_MUST_BE_ZERO = 2n +[@inline] let error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE = 3n +[@inline] let error_MAX_TOKENS_DEPOSITED_MUST_BE_GREATER_THAN_OR_EQUAL_TO_TOKENS_DEPOSITED = 4n +[@inline] let error_LQT_MINTED_MUST_BE_GREATER_THAN_MIN_LQT_MINTED = 5n +(* 6n *) +[@inline] let error_ONLY_NEW_MANAGER_CAN_ACCEPT = 7n +[@inline] let error_CASH_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_BOUGHT = 8n +[@inline] let error_INVALID_TO_ADDRESS = 9n +[@inline] let error_AMOUNT_MUST_BE_ZERO = 10n +[@inline] let error_THE_AMOUNT_OF_CASH_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_WITHDRAWN = 11n +[@inline] let error_LQT_CONTRACT_MUST_HAVE_A_MINT_OR_BURN_ENTRYPOINT = 12n +[@inline] let error_THE_AMOUNT_OF_TOKENS_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_WITHDRAWN = 13n +[@inline] let error_CANNOT_BURN_MORE_THAN_THE_TOTAL_AMOUNT_OF_LQT = 14n +[@inline] let error_TOKEN_POOL_MINUS_TOKENS_WITHDRAWN_IS_NEGATIVE = 15n +[@inline] let error_CASH_POOL_MINUS_CASH_WITHDRAWN_IS_NEGATIVE = 16n +[@inline] let error_CASH_POOL_MINUS_CASH_BOUGHT_IS_NEGATIVE = 17n +[@inline] let error_TOKENS_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_BOUGHT = 18n +[@inline] let error_TOKEN_POOL_MINUS_TOKENS_BOUGHT_IS_NEGATIVE = 19n +[@inline] let error_ONLY_MANAGER_CAN_SET_BAKER = 20n +[@inline] let error_ONLY_MANAGER_CAN_SET_MANAGER = 21n +[@inline] let error_BAKER_PERMANENTLY_FROZEN = 22n +[@inline] let error_LQT_ADDRESS_ALREADY_SET = 24n +[@inline] let error_CALL_NOT_FROM_AN_IMPLICIT_ACCOUNT = 25n +(* 26n *) +(* 27n *) + +[@inline] let error_INVALID_FA12_TOKEN_CONTRACT_MISSING_GETBALANCE = 28n + +[@inline] let error_THIS_ENTRYPOINT_MAY_ONLY_BE_CALLED_BY_GETBALANCE_OF_TOKENADDRESS = 29n +[@inline] let error_INVALID_FA2_BALANCE_RESPONSE = 30n +[@inline] let error_INVALID_INTERMEDIATE_CONTRACT = 31n + +[@inline] let error_CANNOT_GET_CFMM_PRICE_ENTRYPOINT_FROM_CONSUMER = 35n + +(* ============================================================================= + * Constants + * ============================================================================= *) + + [@inline] let null_address = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) + +(* ============================================================================= + * Functions + * ============================================================================= *) + +(* this is slightly inefficient to inline, but, nice to have a clean stack for + the entrypoints for the Coq verification *) +[@inline] +let mutez_to_natural (a: tez) : nat = a / 1mutez + +[@inline] +let natural_to_mutez (a: nat): tez = a * 1mutez + +[@inline] +let is_a_nat (i : int) : nat option = Michelson.is_nat i + +let ceildiv (numerator : nat) (denominator : nat) : nat = abs ((- numerator) / (int denominator)) + +[@inline] +let mint_or_burn (storage : storage) (target : address) (quantity : int) : operation = + (* Returns an operation that mints or burn lqt from the lqt FA1.2 contract. A negative quantity + corresponds to a burn, a positive one to a mint. *) + let lqt_admin : mintOrBurn contract = + match (Tezos.get_entrypoint_opt "%mintOrBurn" storage.lqtAddress : mintOrBurn contract option) with + | None -> (failwith error_LQT_CONTRACT_MUST_HAVE_A_MINT_OR_BURN_ENTRYPOINT : mintOrBurn contract) + | Some contract -> contract in + Tezos.transaction {quantity = quantity ; target = target} 0mutez lqt_admin + +[@inline] +let token_transfer (storage : storage) (from : address) (to_ : address) (token_amount : nat) : operation = + (* Returns an operation that transfers tokens between from and to. *) + let token_contract: token_contract_transfer contract = + match (Tezos.get_entrypoint_opt "%transfer" storage.tokenAddress : token_contract_transfer contract option) with + | None -> (failwith error_TOKEN_CONTRACT_MUST_HAVE_A_TRANSFER_ENTRYPOINT : token_contract_transfer contract) + | Some contract -> contract in + + Tezos.transaction (from, (to_, token_amount)) 0mutez token_contract + +[@inline] + +let cash_transfer (to_ : address) (cash_amount : nat) : operation= + (* Cash transfer operation, in the case where CASH_IS_TEZ *) + let to_contract : unit contract = + match (Tezos.get_contract_opt to_ : unit contract option) with + | None -> (failwith error_INVALID_TO_ADDRESS : unit contract) + | Some c -> c in + Tezos.transaction () (natural_to_mutez cash_amount) to_contract + +(* ============================================================================= + * Entrypoint Functions + * ============================================================================= *) + +(* We assume the contract is originated with at least one liquidity + * provider set up already, so lqtTotal, xtzPool and cashPool will + * always be positive after the initial setup, unless all liquidity is + * removed, at which point the contract is considered dead and stops working + * properly. If this is a concern, at least one address should keep at least a + * very small amount of liquidity in the contract forever. *) + +let add_liquidity (param : add_liquidity) (storage: storage) : result = + (* Adds liquidity to the contract, mints lqt in exchange for the deposited liquidity. *) + let { + owner = owner ; + minLqtMinted = minLqtMinted ; + maxTokensDeposited = maxTokensDeposited ; + + deadline = deadline } = param in + + let cashDeposited = mutez_to_natural Tezos.amount in + + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) + else if Tezos.now >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else + (* The contract is initialized, use the existing exchange rate + mints nothing if the contract has been emptied, but that's OK *) + let cashPool : nat = storage.cashPool in + let lqt_minted : nat = cashDeposited * storage.lqtTotal / cashPool in + let tokens_deposited : nat = ceildiv (cashDeposited * storage.tokenPool) cashPool in + + if tokens_deposited > maxTokensDeposited then + (failwith error_MAX_TOKENS_DEPOSITED_MUST_BE_GREATER_THAN_OR_EQUAL_TO_TOKENS_DEPOSITED : result) + else if lqt_minted < minLqtMinted then + (failwith error_LQT_MINTED_MUST_BE_GREATER_THAN_MIN_LQT_MINTED : result) + else + let storage = {storage with + lqtTotal = storage.lqtTotal + lqt_minted ; + tokenPool = storage.tokenPool + tokens_deposited ; + cashPool = storage.cashPool + cashDeposited} in + + (* send tokens from sender to self *) + let op_token = token_transfer storage Tezos.sender Tezos.self_address tokens_deposited in + + (* mint lqt tokens for them *) + let op_lqt = mint_or_burn storage owner (int lqt_minted) in + + ([op_token; + + op_lqt], storage) + +let remove_liquidity (param : remove_liquidity) (storage : storage) : result = + (* Removes liquidity to the contract by burning lqt. *) + let { to_ = to_ ; + lqtBurned = lqtBurned ; + minCashWithdrawn = minCashWithdrawn ; + minTokensWithdrawn = minTokensWithdrawn ; + deadline = deadline } = param in + + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) + else if Tezos.now >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else if Tezos.amount > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else begin + let cash_withdrawn : nat = (lqtBurned * storage.cashPool) / storage.lqtTotal in + let tokens_withdrawn : nat = (lqtBurned * storage.tokenPool) / storage.lqtTotal in + + (* Check that minimum withdrawal conditions are met *) + if cash_withdrawn < minCashWithdrawn then + (failwith error_THE_AMOUNT_OF_CASH_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_WITHDRAWN : result) + else if tokens_withdrawn < minTokensWithdrawn then + (failwith error_THE_AMOUNT_OF_TOKENS_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_WITHDRAWN : result) + (* Proceed to form the operations and update the storage *) + else begin + (* calculate lqtTotal, convert int to nat *) + let new_lqtTotal = match (is_a_nat ( storage.lqtTotal - lqtBurned)) with + (* This check should be unecessary, the fa12 logic normally takes care of it *) + | None -> (failwith error_CANNOT_BURN_MORE_THAN_THE_TOTAL_AMOUNT_OF_LQT : nat) + | Some n -> n in + (* Calculate tokenPool, convert int to nat *) + let new_tokenPool = match is_a_nat (storage.tokenPool - tokens_withdrawn) with + | None -> (failwith error_TOKEN_POOL_MINUS_TOKENS_WITHDRAWN_IS_NEGATIVE : nat) + | Some n -> n in + let new_cashPool = match is_nat (storage.cashPool - cash_withdrawn) with + | None -> (failwith error_CASH_POOL_MINUS_CASH_WITHDRAWN_IS_NEGATIVE : nat) + | Some n -> n in + let op_lqt = mint_or_burn storage Tezos.sender (0 - lqtBurned) in + let op_token = token_transfer storage Tezos.self_address Tezos.sender tokens_withdrawn in + + let op_cash = cash_transfer to_ cash_withdrawn in + + let storage = {storage with cashPool = new_cashPool ; lqtTotal = new_lqtTotal ; tokenPool = new_tokenPool} in + ([op_lqt; op_token; op_cash], storage) + end + end + +let cash_to_token (param : cash_to_token) (storage : storage) = + let { to_ = to_ ; + minTokensBought = minTokensBought ; + + deadline = deadline } = param in + + let cashSold = mutez_to_natural Tezos.amount in + + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) + else if Tezos.now >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else begin + (* We don't check that xtzPool > 0, because that is impossible + unless all liquidity has been removed. *) + let cashPool = storage.cashPool in + let tokens_bought = + (let bought = (cashSold * const_fee * storage.tokenPool) / (cashPool * const_fee_denom + (cashSold * const_fee)) in + if bought < minTokensBought then + (failwith error_TOKENS_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_BOUGHT : nat) + else + bought) + in + let new_tokenPool = (match is_nat (storage.tokenPool - tokens_bought) with + | None -> (failwith error_TOKEN_POOL_MINUS_TOKENS_BOUGHT_IS_NEGATIVE : nat) + | Some difference -> difference) in + + (* Update cashPool. *) + let storage = { storage with cashPool = storage.cashPool + cashSold ; tokenPool = new_tokenPool } in + (* Send cash from sender to self. *) + + (* Send tokens_withdrawn from exchange to sender. *) + let op_token = token_transfer storage Tezos.self_address to_ tokens_bought in + ([ + + op_token], storage) + end + +let token_to_cash (param : token_to_cash) (storage : storage) = + (* Accepts a payment in token and sends cash. *) + let { to_ = to_ ; + tokensSold = tokensSold ; + minCashBought = minCashBought ; + deadline = deadline } = param in + + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) + else if Tezos.now >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else if Tezos.amount > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else + (* We don't check that tokenPool > 0, because that is impossible + unless all liquidity has been removed. *) + let cash_bought = + let bought = ((tokensSold * const_fee * storage.cashPool) / (storage.tokenPool * const_fee_denom + (tokensSold * const_fee))) in + if bought < minCashBought then (failwith error_CASH_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_BOUGHT : nat) else bought in + + let op_token = token_transfer storage Tezos.sender Tezos.self_address tokensSold in + + let op_cash = cash_transfer to_ cash_bought in + + let new_cashPool = match is_nat (storage.cashPool - cash_bought) with + | None -> (failwith error_ASSERTION_VIOLATED_CASH_BOUGHT_SHOULD_BE_LESS_THAN_CASHPOOL : nat) + | Some n -> n in + let storage = {storage with tokenPool = storage.tokenPool + tokensSold ; + cashPool = new_cashPool} in + ([op_token; op_cash], storage) + +let default_ (storage : storage) : result = +(* Entrypoint to allow depositing tez. *) + + (* update cashPool *) + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO: result) + else + let storage = {storage with cashPool = storage.cashPool + mutez_to_natural Tezos.amount } in + (([] : operation list), storage) + +let set_lqt_address (lqtAddress : address) (storage : storage) : result = + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) + else if Tezos.amount > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else if storage.lqtAddress <> null_address then + (failwith error_LQT_ADDRESS_ALREADY_SET : result) + else + (([] : operation list), {storage with lqtAddress = lqtAddress}) + +let update_pools (storage : storage) : result = + (* Update the token pool and potentially the cash pool if cash is a token. *) + if Tezos.sender <> Tezos.source then + (failwith error_CALL_NOT_FROM_AN_IMPLICIT_ACCOUNT : result) + else if Tezos.amount > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else + let cfmm_update_token_pool_internal : update_token_pool_internal contract = Tezos.self "%updateTokenPoolInternal" in + + let token_get_balance : get_balance contract = (match + (Tezos.get_entrypoint_opt "%getBalance" storage.tokenAddress : get_balance contract option) with + | None -> (failwith error_INVALID_FA12_TOKEN_CONTRACT_MISSING_GETBALANCE : get_balance contract) + | Some contract -> contract) in + let op = Tezos.transaction (Tezos.self_address, cfmm_update_token_pool_internal) 0mutez token_get_balance in + + let op_list = [ op ] in + + let pendingPoolUpdates = 1n in + + (op_list, {storage with pendingPoolUpdates = pendingPoolUpdates}) + +[@inline] +let update_fa12_pool_internal (pool_update : update_fa12_pool) : nat = + pool_update + +[@inline] +let update_fa2_pool_internal (pool_update : update_fa2_pool) : nat = + (* We trust the FA2 to provide the expected balance. there are no BFS + shenanigans to worry about unless the token contract misbehaves. *) + match pool_update with + | [] -> (failwith error_INVALID_FA2_BALANCE_RESPONSE : nat) + | x :: xs -> x.1 + +let update_token_pool_internal (pool_update : update_token_pool_internal) (storage : storage) : result = + if (storage.pendingPoolUpdates = 0n or Tezos.sender <> storage.tokenAddress) then + (failwith error_THIS_ENTRYPOINT_MAY_ONLY_BE_CALLED_BY_GETBALANCE_OF_TOKENADDRESS : result) + else + + let pool = update_fa12_pool_internal (pool_update) in + + let pendingPoolUpdates = abs (storage.pendingPoolUpdates - 1n) in + (([] : operation list), {storage with tokenPool = pool ; pendingPoolUpdates = pendingPoolUpdates}) + +let token_to_token (param : token_to_token) (storage : storage) : result = + let { outputCfmmContract = outputCfmmContract ; + minTokensBought = minTokensBought ; + to_ = to_ ; + tokensSold = tokensSold ; + deadline = deadline } = param in + + let outputCfmmContract_contract: cash_to_token contract = + (match (Tezos.get_entrypoint_opt "%cashToToken" outputCfmmContract : cash_to_token contract option) with + | None -> (failwith error_INVALID_INTERMEDIATE_CONTRACT : cash_to_token contract) + | Some c -> c) in + + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) + else if Tezos.amount > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else if Tezos.now >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else + (* We don't check that tokenPool > 0, because that is impossible unless all liquidity has been removed. *) + let cash_bought = ((tokensSold * const_fee * storage.cashPool) / (storage.tokenPool * const_fee_denom + (tokensSold * const_fee))) in + let new_cashPool = match is_nat (storage.cashPool - cash_bought) with + | None -> (failwith error_CASH_POOL_MINUS_CASH_BOUGHT_IS_NEGATIVE : nat) + | Some n -> n in + let storage = {storage with tokenPool = storage.tokenPool + tokensSold ; + cashPool = new_cashPool } in + + let op_send_cash_to_output = Tezos.transaction { minTokensBought = minTokensBought ; + deadline = deadline; to_ = to_ } + (natural_to_mutez cash_bought) + outputCfmmContract_contract in + + let op_accept_token_from_sender = token_transfer storage Tezos.sender Tezos.self_address tokensSold in + ([ + + op_send_cash_to_output; op_accept_token_from_sender] , storage) + +(* For the ctez contract only, accepts tez and calls another cfmm for which cash_is_ctez *) + +type tez_to_token = + [@layout:comb] + { outputCfmmContract : address ; (* other cfmm contract *) + minTokensBought : nat ; (* minimum amount of tokens bought *) + [@annot:to] to_ : address ; (* where to send the output tokens *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +type ctez_to_token = + [@layout:comb] + { [@annot:to] to_ : address ; (* where to send the tokens *) + minTokensBought : nat ; (* minimum amount of tokens that must be bought *) + cashSold : nat ; + deadline : timestamp ; + } + +let tez_to_token (param : tez_to_token) (storage : storage) : result = + + let { outputCfmmContract = outputCfmmContract ; + minTokensBought = minTokensBought ; + to_ = to_ ; + deadline = deadline } = param in + + let outputCfmmContract_contract: ctez_to_token contract = + (match (Tezos.get_entrypoint_opt "%cashToToken" outputCfmmContract : ctez_to_token contract option) with + | None -> (failwith error_INVALID_INTERMEDIATE_CONTRACT : ctez_to_token contract) + | Some c -> c) in + + if storage.pendingPoolUpdates > 0n then + (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) + else if Tezos.now >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else + let tezSold = Tezos.amount in + (* We don't check that tokenPool > 0, because that is impossible unless all liquidity has been removed. *) + let ctez_bought = ((tezSold * const_fee * storage.tokenPool) / (storage.cashPool * const_fee_denom + (tezSold * const_fee))) in + let new_tokenPool = match is_nat (storage.tokenPool - ctez_bought) with + | None -> (failwith error_TOKEN_POOL_MINUS_TOKENS_BOUGHT_IS_NEGATIVE : nat) + | Some n -> n in + let storage = {storage with tokenPool = storage.tokenPool + tokensSold ; + cashPool = new_cashPool } in + + let op_send_cash_to_output = Tezos.transaction { minTokensBought = minTokensBought ; + deadline = deadline; to_ = to_ } + (natural_to_mutez cash_bought) + outputCfmmContract_contract in + + let op_accept_token_from_sender = token_transfer storage Tezos.sender Tezos.self_address tokensSold in + ([ + + op_send_cash_to_output; op_accept_token_from_sender] , storage) + + + +let update_consumer (operations, storage : result) : result = + if storage.lastOracleUpdate = Tezos.now + then (operations, storage) + else + let consumer = match (Tezos.get_contract_opt storage.consumerEntrypoint : ((nat * nat) contract) option) with + | None -> (failwith error_CANNOT_GET_CFMM_PRICE_ENTRYPOINT_FROM_CONSUMER : (nat * nat) contract) + | Some c -> c in + ((Tezos.transaction (storage.cashPool, storage.tokenPool) 0mutez consumer) :: operations, + {storage with lastOracleUpdate = Tezos.now}) + +(* ============================================================================= + * Main + * ============================================================================= *) + +let main ((entrypoint, storage) : entrypoint * storage) : result = + match entrypoint with + | AddLiquidity param -> + add_liquidity param storage + | RemoveLiquidity param -> + remove_liquidity param storage + | UpdatePools -> + update_pools storage + | CashToToken param -> + update_consumer + (cash_to_token param storage) + | TokenToCash param -> + update_consumer + (token_to_cash param storage) + | TokenToToken param -> + update_consumer + (token_to_token param storage) + | UpdateTokenPoolInternal token_pool -> + update_token_pool_internal token_pool storage + | SetLqtAddress param -> + set_lqt_address param storage \ No newline at end of file diff --git a/cfmm.mligo b/cfmm.mligo index 4fef0f16..21336948 100644 --- a/cfmm.mligo +++ b/cfmm.mligo @@ -1,12 +1,12 @@ -(* Pick one of CASH_IS_TEZ, CASH_IS_FA2, CASH_IS_FA12. tokenToToken isn't supported for CASH_IS_FA12 *) -//#define CASH_IS_TEZ +#include "errors.mligo" + +(* Pick one of CASH_IS_FA2, CASH_IS_FA12. tokenToToken isn't supported for CASH_IS_FA2 *) //#define CASH_IS_FA2 //#define CASH_IS_FA12 (* If the token uses the fa2 standard *) //#define TOKEN_IS_FA2 -(* To support baking *) -//#define HAS_BAKER + (* To push prices to some consumer contract once per block *) //#define ORACLE @@ -20,9 +20,7 @@ type add_liquidity = { owner : address ; (* address that will own the minted lqt *) minLqtMinted : nat ; (* minimum number of lqt that must be minter *) maxTokensDeposited : nat ; (* maximum number of tokens that may be deposited *) -#if !CASH_IS_TEZ cashDeposited : nat ; (* if cash isn't tez, specifiy the amount to be deposited *) -#endif deadline : timestamp ; (* time before which the request must be completed *) } @@ -39,9 +37,7 @@ type cash_to_token = [@layout:comb] { [@annot:to] to_ : address ; (* where to send the tokens *) minTokensBought : nat ; (* minimum amount of tokens that must be bought *) -#if !CASH_IS_TEZ cashSold : nat ; (* if cash isn't tez, how much cash is sought to be sold *) -#endif deadline : timestamp ; (* time before which the request must be completed *) } @@ -63,13 +59,6 @@ type token_to_token = deadline : timestamp ; (* time before which the request must be completed *) } -#if HAS_BAKER -type set_baker = - [@layout:comb] - { baker : key_hash option ; (* delegate address, None if undelegated *) - freezeBaker : bool ; (* whether to permanently freeze the baker *) - } -#endif (* getbalance update types for fa12 and fa2 *) type update_fa12_pool = nat @@ -97,15 +86,7 @@ type entrypoint = | TokenToToken of token_to_token | UpdatePools of unit | UpdateTokenPoolInternal of update_token_pool_internal -#if HAS_BAKER -| SetBaker of set_baker -| SetManager of address -| AcceptManager of unit -| Default of unit -#endif -#if !CASH_IS_TEZ | UpdateCashPoolInternal of update_cash_pool_internal -#endif | SetLqtAddress of address @@ -119,18 +100,11 @@ type storage = cashPool : nat ; lqtTotal : nat ; pendingPoolUpdates : nat ; -#if HAS_BAKER - freezeBaker : bool ; - manager : address ; - new_manager : address ; -#endif tokenAddress : address ; #if TOKEN_IS_FA2 tokenId : nat ; #endif -#if !CASH_IS_TEZ cashAddress : address ; -#endif #if CASH_IS_FA2 cashId : nat ; #endif @@ -171,58 +145,6 @@ type mintOrBurn = { quantity : int ; target : address } -(* ============================================================================= - * Error codes - * ============================================================================= *) - -[@inline] let error_TOKEN_CONTRACT_MUST_HAVE_A_TRANSFER_ENTRYPOINT = 0n -[@inline] let error_ASSERTION_VIOLATED_CASH_BOUGHT_SHOULD_BE_LESS_THAN_CASHPOOL = 1n -[@inline] let error_PENDING_POOL_UPDATES_MUST_BE_ZERO = 2n -[@inline] let error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE = 3n -[@inline] let error_MAX_TOKENS_DEPOSITED_MUST_BE_GREATER_THAN_OR_EQUAL_TO_TOKENS_DEPOSITED = 4n -[@inline] let error_LQT_MINTED_MUST_BE_GREATER_THAN_MIN_LQT_MINTED = 5n -(* 6n *) -[@inline] let error_ONLY_NEW_MANAGER_CAN_ACCEPT = 7n -[@inline] let error_CASH_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_BOUGHT = 8n -[@inline] let error_INVALID_TO_ADDRESS = 9n -[@inline] let error_AMOUNT_MUST_BE_ZERO = 10n -[@inline] let error_THE_AMOUNT_OF_CASH_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_WITHDRAWN = 11n -[@inline] let error_LQT_CONTRACT_MUST_HAVE_A_MINT_OR_BURN_ENTRYPOINT = 12n -[@inline] let error_THE_AMOUNT_OF_TOKENS_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_WITHDRAWN = 13n -[@inline] let error_CANNOT_BURN_MORE_THAN_THE_TOTAL_AMOUNT_OF_LQT = 14n -[@inline] let error_TOKEN_POOL_MINUS_TOKENS_WITHDRAWN_IS_NEGATIVE = 15n -[@inline] let error_CASH_POOL_MINUS_CASH_WITHDRAWN_IS_NEGATIVE = 16n -[@inline] let error_CASH_POOL_MINUS_CASH_BOUGHT_IS_NEGATIVE = 17n -[@inline] let error_TOKENS_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_BOUGHT = 18n -[@inline] let error_TOKEN_POOL_MINUS_TOKENS_BOUGHT_IS_NEGATIVE = 19n -[@inline] let error_ONLY_MANAGER_CAN_SET_BAKER = 20n -[@inline] let error_ONLY_MANAGER_CAN_SET_MANAGER = 21n -[@inline] let error_BAKER_PERMANENTLY_FROZEN = 22n -[@inline] let error_LQT_ADDRESS_ALREADY_SET = 24n -[@inline] let error_CALL_NOT_FROM_AN_IMPLICIT_ACCOUNT = 25n -(* 26n *) -(* 27n *) -#if TOKEN_IS_FA2 -[@inline] let error_INVALID_FA2_TOKEN_CONTRACT_MISSING_BALANCE_OF = 28n -#else -[@inline] let error_INVALID_FA12_TOKEN_CONTRACT_MISSING_GETBALANCE = 28n -#endif -[@inline] let error_THIS_ENTRYPOINT_MAY_ONLY_BE_CALLED_BY_GETBALANCE_OF_TOKENADDRESS = 29n -[@inline] let error_INVALID_FA2_BALANCE_RESPONSE = 30n -[@inline] let error_INVALID_INTERMEDIATE_CONTRACT = 31n -#if !CASH_IS_TEZ -[@inline] let error_THIS_ENTRYPOINT_MAY_ONLY_BE_CALLED_BY_GETBALANCE_OF_CASHADDRESS = 30n -[@inline] let error_TEZ_DEPOSIT_WOULD_BE_BURNED = 32n -#if CASH_IS_FA2 -[@inline] let error_INVALID_FA2_CASH_CONTRACT_MISSING_GETBALANCE = 33n -#else -[@inline] let error_INVALID_FA12_CASH_CONTRACT_MISSING_GETBALANCE = 33n -[@inline] let error_MISSING_APPROVE_ENTRYPOINT_IN_CASH_CONTRACT = 34n -#endif -#endif -#if ORACLE -[@inline] let error_CANNOT_GET_CFMM_PRICE_ENTRYPOINT_FROM_CONSUMER = 35n -#endif (* ============================================================================= * Constants @@ -271,15 +193,7 @@ let token_transfer (storage : storage) (from : address) (to_ : address) (token_a #endif [@inline] -#if CASH_IS_TEZ -let cash_transfer (to_ : address) (cash_amount : nat) : operation= - (* Cash transfer operation, in the case where CASH_IS_TEZ *) - let to_contract : unit contract = - match (Tezos.get_contract_opt to_ : unit contract option) with - | None -> (failwith error_INVALID_TO_ADDRESS : unit contract) - | Some c -> c in - Tezos.transaction () (natural_to_mutez cash_amount) to_contract -#else + let cash_transfer (storage : storage) (from : address) (to_ : address) (cash_amount : nat) : operation= (* Cash transfer operation, in the case where cash is some fa2 or fa12 token *) let cash_contract: cash_contract_transfer contract = @@ -291,7 +205,6 @@ let cash_transfer (storage : storage) (from : address) (to_ : address) (cash_amo #else Tezos.transaction (from, (to_, cash_amount)) 0mutez cash_contract #endif -#endif (* ============================================================================= * Entrypoint Functions @@ -310,13 +223,8 @@ let add_liquidity (param : add_liquidity) (storage: storage) : result = owner = owner ; minLqtMinted = minLqtMinted ; maxTokensDeposited = maxTokensDeposited ; -#if !CASH_IS_TEZ cashDeposited = cashDeposited ; -#endif deadline = deadline } = param in -#if CASH_IS_TEZ - let cashDeposited = mutez_to_natural Tezos.amount in -#endif if storage.pendingPoolUpdates > 0n then (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) else if Tezos.now >= deadline then @@ -340,17 +248,13 @@ let add_liquidity (param : add_liquidity) (storage: storage) : result = (* send tokens from sender to self *) let op_token = token_transfer storage Tezos.sender Tezos.self_address tokens_deposited in -#if !CASH_IS_TEZ (* send cash from sender to self *) let op_cash = cash_transfer storage Tezos.sender Tezos.self_address cashDeposited in -#endif (* mint lqt tokens for them *) let op_lqt = mint_or_burn storage owner (int lqt_minted) in ([op_token; -#if !CASH_IS_TEZ op_cash; -#endif op_lqt], storage) let remove_liquidity (param : remove_liquidity) (storage : storage) : result = @@ -392,11 +296,7 @@ let remove_liquidity (param : remove_liquidity) (storage : storage) : result = | Some n -> n in let op_lqt = mint_or_burn storage Tezos.sender (0 - lqtBurned) in let op_token = token_transfer storage Tezos.self_address Tezos.sender tokens_withdrawn in -#if CASH_IS_TEZ - let op_cash = cash_transfer to_ cash_withdrawn in -#else let op_cash = cash_transfer storage Tezos.self_address to_ cash_withdrawn in -#endif let storage = {storage with cashPool = new_cashPool ; lqtTotal = new_lqtTotal ; tokenPool = new_tokenPool} in ([op_lqt; op_token; op_cash], storage) end @@ -406,14 +306,9 @@ let remove_liquidity (param : remove_liquidity) (storage : storage) : result = let cash_to_token (param : cash_to_token) (storage : storage) = let { to_ = to_ ; minTokensBought = minTokensBought ; -#if !CASH_IS_TEZ cashSold = cashSold ; -#endif deadline = deadline } = param in -#if CASH_IS_TEZ - let cashSold = mutez_to_natural Tezos.amount in -#endif if storage.pendingPoolUpdates > 0n then (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) else if Tezos.now >= deadline then @@ -436,16 +331,10 @@ let cash_to_token (param : cash_to_token) (storage : storage) = (* Update cashPool. *) let storage = { storage with cashPool = storage.cashPool + cashSold ; tokenPool = new_tokenPool } in (* Send cash from sender to self. *) -#if !CASH_IS_TEZ let op_cash = cash_transfer storage Tezos.sender Tezos.self_address cashSold in -#endif (* Send tokens_withdrawn from exchange to sender. *) let op_token = token_transfer storage Tezos.self_address to_ tokens_bought in - ([ -#if !CASH_IS_TEZ - op_cash; -#endif - op_token], storage) + ([op_cash; op_token], storage) end @@ -470,11 +359,7 @@ let token_to_cash (param : token_to_cash) (storage : storage) = if bought < minCashBought then (failwith error_CASH_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_BOUGHT : nat) else bought in let op_token = token_transfer storage Tezos.sender Tezos.self_address tokensSold in -#if CASH_IS_TEZ - let op_cash = cash_transfer to_ cash_bought in -#else let op_cash = cash_transfer storage Tezos.self_address to_ cash_bought in -#endif let new_cashPool = match is_nat (storage.cashPool - cash_bought) with | None -> (failwith error_ASSERTION_VIOLATED_CASH_BOUGHT_SHOULD_BE_LESS_THAN_CASHPOOL : nat) | Some n -> n in @@ -485,51 +370,8 @@ let token_to_cash (param : token_to_cash) (storage : storage) = let default_ (storage : storage) : result = (* Entrypoint to allow depositing tez. *) -#if CASH_IS_TEZ - (* update cashPool *) - if storage.pendingPoolUpdates > 0n then - (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO: result) - else - let storage = {storage with cashPool = storage.cashPool + mutez_to_natural Tezos.amount } in - (([] : operation list), storage) -#else (failwith error_TEZ_DEPOSIT_WOULD_BE_BURNED : result) -#endif - -#if HAS_BAKER -let set_baker (param : set_baker) (storage : storage) : result = - let { baker = baker ; - freezeBaker = freezeBaker } = param in - if storage.pendingPoolUpdates > 0n then - (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) - else if Tezos.amount > 0mutez then - (failwith error_AMOUNT_MUST_BE_ZERO : result) - else if Tezos.sender <> storage.manager then - (failwith error_ONLY_MANAGER_CAN_SET_BAKER : result) - else if storage.freezeBaker then - (failwith error_BAKER_PERMANENTLY_FROZEN : result) - else - ([ Tezos.set_delegate baker ], {storage with freezeBaker = freezeBaker}) -let set_manager (new_manager : address) (storage : storage) : result = - if storage.pendingPoolUpdates > 0n then - (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) - else if Tezos.amount > 0mutez then - (failwith error_AMOUNT_MUST_BE_ZERO : result) - else if Tezos.sender <> storage.manager then - (failwith error_ONLY_MANAGER_CAN_SET_MANAGER : result) - else - (([] : operation list), {storage with new_manager = new_manager}) - -let accept_manager (storage : storage) : result = - if storage.pendingPoolUpdates > 0n then - (failwith error_PENDING_POOL_UPDATES_MUST_BE_ZERO : result) - else if Tezos.sender <> storage.new_manager then - (failwith error_ONLY_NEW_MANAGER_CAN_ACCEPT : result) - else - (([] : operation list), {storage with manager = storage.new_manager}) - -#endif let set_lqt_address (lqtAddress : address) (storage : storage) : result = if storage.pendingPoolUpdates > 0n then @@ -549,9 +391,7 @@ let update_pools (storage : storage) : result = (failwith error_AMOUNT_MUST_BE_ZERO : result) else let cfmm_update_token_pool_internal : update_token_pool_internal contract = Tezos.self "%updateTokenPoolInternal" in -#if !CASH_IS_TEZ let cfmm_update_cash_pool_internal : update_cash_pool_internal contract = Tezos.self "%updateCashPoolInternal" in -#endif #if TOKEN_IS_FA2 let token_balance_of : balance_of contract = (match (Tezos.get_entrypoint_opt "%balance_of" storage.tokenAddress : balance_of contract option) with @@ -582,11 +422,7 @@ let update_pools (storage : storage) : result = let op_cash = Tezos.transaction ([(Tezos.self_address, storage.cashId)], cfmm_update_cash_pool_internal) 0mutez cash_balance_of in let op_list = op_cash :: op_list in #endif -#if CASH_IS_TEZ - let pendingPoolUpdates = 1n in -#else let pendingPoolUpdates = 2n in -#endif (op_list, {storage with pendingPoolUpdates = pendingPoolUpdates}) @@ -614,7 +450,6 @@ let update_token_pool_internal (pool_update : update_token_pool_internal) (stora let pendingPoolUpdates = abs (storage.pendingPoolUpdates - 1n) in (([] : operation list), {storage with tokenPool = pool ; pendingPoolUpdates = pendingPoolUpdates}) -#if !CASH_IS_TEZ let update_cash_pool_internal (pool_update : update_cash_pool_internal) (storage : storage) : result = if (storage.pendingPoolUpdates = 0n or Tezos.sender <> storage.cashAddress) then (failwith error_THIS_ENTRYPOINT_MAY_ONLY_BE_CALLED_BY_GETBALANCE_OF_CASHADDRESS : result) @@ -626,7 +461,6 @@ let update_cash_pool_internal (pool_update : update_cash_pool_internal) (storage #endif let pendingPoolUpdates = abs (storage.pendingPoolUpdates - 1) in (([] : operation list), {storage with cashPool = pool ; pendingPoolUpdates = pendingPoolUpdates}) -#endif let token_to_token (param : token_to_token) (storage : storage) : result = let { outputCfmmContract = outputCfmmContract ; @@ -655,12 +489,6 @@ let token_to_token (param : token_to_token) (storage : storage) : result = let storage = {storage with tokenPool = storage.tokenPool + tokensSold ; cashPool = new_cashPool } in -#if CASH_IS_TEZ - let op_send_cash_to_output = Tezos.transaction { minTokensBought = minTokensBought ; - deadline = deadline; to_ = to_ } - (natural_to_mutez cash_bought) - outputCfmmContract_contract in -#else let allow_output_to_withdraw_cash = #if CASH_IS_FA12 let cashContract_approve = (match (Tezos.get_entrypoint_opt "%approve" storage.cashAddress : (address * nat) contract option) with @@ -680,12 +508,10 @@ let token_to_token (param : token_to_token) (storage : storage) : result = deadline = deadline ; to_ = to_} 0mutez outputCfmmContract_contract in -#endif let op_accept_token_from_sender = token_transfer storage Tezos.sender Tezos.self_address tokensSold in ([ -#if !CASH_IS_TEZ + allow_output_to_withdraw_cash.0 ; allow_output_to_withdraw_cash.1 ; -#endif op_send_cash_to_output; op_accept_token_from_sender] , storage) @@ -711,20 +537,8 @@ let main ((entrypoint, storage) : entrypoint * storage) : result = add_liquidity param storage | RemoveLiquidity param -> remove_liquidity param storage -#if HAS_BAKER - | SetBaker param -> - set_baker param storage - | SetManager param -> - set_manager param storage - | AcceptManager -> - accept_manager storage - | Default -> - default_ storage -#endif -#if !CASH_IS_TEZ | UpdateCashPoolInternal cash_pool -> update_cash_pool_internal cash_pool storage -#endif | UpdatePools -> update_pools storage | CashToToken param -> @@ -745,4 +559,4 @@ let main ((entrypoint, storage) : entrypoint * storage) : result = | UpdateTokenPoolInternal token_pool -> update_token_pool_internal token_pool storage | SetLqtAddress param -> - set_lqt_address param storage + set_lqt_address param storage \ No newline at end of file diff --git a/cfmm_initial_storage.ml b/cfmm_initial_storage.ml new file mode 100644 index 00000000..b55b662a --- /dev/null +++ b/cfmm_initial_storage.ml @@ -0,0 +1,10 @@ +{ tokenPool = 1n ; + cashPool = 1n ; + lqtTotal = 1n ; + pendingPoolUpdates = 0n ; + tokenAddress = ("FA12_CTEZ" : address) ; + lqtAddress = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) ; + lastOracleUpdate = ("2021-01-01T00:00:00Z" : timestamp) ; + consumerEntrypoint = ("CTEZ_ADDRESS%cfmm_price" : address) ; + } + diff --git a/cfmm_initial_storage.mligo b/cfmm_initial_storage.mligo index b55b662a..6033f01e 100644 --- a/cfmm_initial_storage.mligo +++ b/cfmm_initial_storage.mligo @@ -1,8 +1,9 @@ -{ tokenPool = 1n ; +{ tezPool = 1n ; cashPool = 1n ; + target = Bitwise.shift_left 1n 48n; lqtTotal = 1n ; - pendingPoolUpdates = 0n ; - tokenAddress = ("FA12_CTEZ" : address) ; + ctez_address = ("CTEZ_ADDRESS" : address) ; + cashAddress = ("FA12_CTEZ" : address) ; lqtAddress = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) ; lastOracleUpdate = ("2021-01-01T00:00:00Z" : timestamp) ; consumerEntrypoint = ("CTEZ_ADDRESS%cfmm_price" : address) ; diff --git a/cfmm_tez_ctez.ml b/cfmm_tez_ctez.ml new file mode 100644 index 00000000..83088093 --- /dev/null +++ b/cfmm_tez_ctez.ml @@ -0,0 +1,5 @@ +#define ORACLE +#define CASH_IS_TEZ +[@inline] let const_fee = 9995n (* 0.05% fee *) +[@inline] let const_fee_denom = 10000n +#include "cfmm.mligo" \ No newline at end of file diff --git a/cfmm_tez_ctez.mligo b/cfmm_tez_ctez.mligo index 83088093..0106566d 100644 --- a/cfmm_tez_ctez.mligo +++ b/cfmm_tez_ctez.mligo @@ -1,5 +1,479 @@ -#define ORACLE -#define CASH_IS_TEZ -[@inline] let const_fee = 9995n (* 0.05% fee *) +#include "errors.mligo" +#include "newton.mligo" + +(* Check attic/cfmm_tez_ctez.old.preprocessed.mligo to compare with the old version of ctez *) + +[@inline] let error_CALLER_MUST_BE_CTEZ = 1000n +[@inline] let error_ASSERTION_VIOLATED_TEZ_BOUGHT_SHOULD_BE_LESS_THAN_TEZPOOL = 1001n + +(* ============================================================================ + * Entrypoints + * ============================================================================ *) + +type add_liquidity = + [@layout:comb] + { owner : address ; (* address that will own the minted lqt *) + minLqtMinted : nat ; (* minimum number of lqt that must be minted *) + maxCashDeposited : nat ; (* maximum number of cash that may be deposited *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +type remove_liquidity = + [@layout:comb] + { [@annot:to] to_ : address ; (* recipient of the liquidity redemption *) + lqtBurned : nat ; (* amount of lqt owned by sender to burn *) + minTezWithdrawn : nat ; (* minimum amount of tez to withdraw *) + minCashWithdrawn : nat ; (* minimum amount of cash to withdraw *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +type tez_to_cash = + [@layout:comb] + { [@annot:to] to_ : address ; (* where to send the cash *) + minCashBought : nat ; (* minimum amount of cash that must be bought *) + deadline : timestamp ; (* time before which the request must be completed *) + rounds : int ; (* number of iterations in estimating the difference equations. Default should be 4n. *) + } + +type cash_to_tez = + [@layout:comb] + { [@annot:to] to_ : address ; (* where to send the tez *) + cashSold : nat ; (* how many cash are being sold *) + minTezBought : nat ; (* minimum amount of tez desired *) + deadline : timestamp ; (* time before which the request must be completed *) + rounds : int ; (* number of iterations in estimating the difference equations. Default should be 4n. *) + } + +type cash_to_token = + [@layout:comb] + { [@annot:to] to_ : address ; (* where to send the tokens *) + minTokensBought : nat ; (* minimum amount of tokens that must be bought *) + cashSold : nat ; (* if cash isn't tez, how much cash is sought to be sold *) + deadline : timestamp ; (* time before which the request must be completed *) + } + +type tez_to_token = + [@layout:comb] + { outputCfmmContract : address ; (* other cfmm contract *) + minTokensBought : nat ; (* minimum amount of cash bought *) + [@annot:to] to_ : address ; (* where to send the output cash *) + deadline : timestamp ; (* time before which the request must be completed *) + rounds : int ; (* number of iterations in estimating the difference equations. Default should be 4n. *) + } + + +(* A target t such that t / 2^48 is the target price from the ctez contract, and the number of newly minted ctez *) +type ctez_target = nat * nat + +(* Marginal price, as a pair containing the numerator and the denominator *) +type get_marginal_price = (nat * nat) contract + +type entrypoint = +| AddLiquidity of add_liquidity +| RemoveLiquidity of remove_liquidity +| CtezTarget of ctez_target +| TezToCash of tez_to_cash +| CashToTez of cash_to_tez +| TezToToken of tez_to_token +| SetLqtAddress of address +| GetMarginalPrice of get_marginal_price + + +(* ============================================================================= + * Storage + * ============================================================================= *) + +type storage = + [@layout:comb] + { cashPool : nat ; + tezPool : nat ; + lqtTotal : nat ; + target : nat ; + ctez_address : address ; + cashAddress : address ; + lqtAddress : address ; + lastOracleUpdate : timestamp ; + consumerEntrypoint : address ; + } + +(* Type Synonyms *) + +type result = operation list * storage + +(* FA2 *) +type cash_id = nat +type balance_of = ((address * cash_id) list * ((((address * nat) * nat) list) contract)) +(* FA1.2 *) +type get_balance = address * (nat contract) + +type cash_contract_transfer = address * (address * nat) + +(* custom entrypoint for LQT FA1.2 *) +type mintOrBurn = + [@layout:comb] + { quantity : int ; + target : address } + + + + + +(* ============================================================================= + * Constants + * ============================================================================= *) + +[@inline] let null_address = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) +[@inline] let const_fee = 9999n (* 0.01% fee *) [@inline] let const_fee_denom = 10000n -#include "cfmm.mligo" \ No newline at end of file + + +(* ============================================================================= + * Functions + * ============================================================================= *) + +(* this is slightly inefficient to inline, but, nice to have a clean stack for + the entrypoints for the Coq verification *) +[@inline] +let mutez_to_natural (a: tez) : nat = a / 1mutez + +[@inline] +let natural_to_mutez (a: nat): tez = a * 1mutez + + +let ceildiv (numerator : nat) (denominator : nat) : nat = abs ((- numerator) / (int denominator)) + +[@inline] +let mint_or_burn (storage : storage) (target : address) (quantity : int) : operation = + (* Returns an operation that mints or burn lqt from the lqt FA1.2 contract. A negative quantity + corresponds to a burn, a positive one to a mint. *) + let lqt_admin : mintOrBurn contract = + match (Tezos.get_entrypoint_opt "%mintOrBurn" storage.lqtAddress : mintOrBurn contract option) with + | None -> (failwith error_LQT_CONTRACT_MUST_HAVE_A_MINT_OR_BURN_ENTRYPOINT : mintOrBurn contract) + | Some contract -> contract in + Tezos.transaction {quantity = quantity ; target = target} 0mutez lqt_admin + +[@inline] +let cash_transfer (storage : storage) (from : address) (to_ : address) (cash_amount : nat) : operation = + (* Returns an operation that transfers cash between from and to. *) + let cash_contract: cash_contract_transfer contract = + match (Tezos.get_entrypoint_opt "%transfer" storage.cashAddress : cash_contract_transfer contract option) with + | None -> (failwith error_CASH_CONTRACT_MUST_HAVE_A_TRANSFER_ENTRYPOINT : cash_contract_transfer contract) + | Some contract -> contract in + Tezos.transaction (from, (to_, cash_amount)) 0mutez cash_contract + +[@inline] +let tez_transfer (to_ : address) (tez_amount : nat) : operation= + (* Tez transfer operation, in the case where TEZ_IS_TEZ *) + let to_contract : unit contract = + match (Tezos.get_contract_opt to_ : unit contract option) with + | None -> (failwith error_INVALID_TO_ADDRESS : unit contract) + | Some c -> c in + Tezos.transaction () (natural_to_mutez tez_amount) to_contract + +// A function that outputs dy (diff_cash) given x, y, and dx +let trade_dtez_for_dcash (tez : nat) (cash : nat) (dtez : nat) (target : nat) (rounds : int) : nat = + let x = Bitwise.shift_left tez 48n in + let y = target * cash in + let dx = Bitwise.shift_left dtez 48n in + let dy_approx = newton_dx_to_dy (x, y, dx, rounds) in + let dcash_approx = dy_approx / target in + if (cash - dcash_approx <= 0) then + (failwith error_CASH_POOL_MINUS_CASH_WITHDRAWN_IS_NEGATIVE : nat) + else + dcash_approx + + +// A function that outputs dx (diff_tez) given target, x, y, and dy +let trade_dcash_for_dtez (tez : nat) (cash : nat) (dcash : nat) (target : nat) (rounds : int) : nat = + let x = target * cash in + let y = Bitwise.shift_left tez 48n in + let dx = target * dcash in + let dy_approx = newton_dx_to_dy (x, y, dx, rounds) in + let dtez_approx = Bitwise.shift_right dy_approx 48n in + if (tez - dtez_approx <= 0) then + (failwith error_TEZ_POOL_MINUS_TEZ_WITHDRAWN_IS_NEGATIVE : nat) (* should never happen *) + else + dtez_approx + + +// Marginal price of cash in tez +let marginal_price (tez : nat) (cash : nat) (target : nat) : (nat * nat) = + let x = cash * target in + let y = Bitwise.shift_left tez 48n in + let (num, den) = margin x y in (* how many tez do I get for my cash *) + (Bitwise.shift_left num 48n, den * target) + +(* ======== + * Views + *) + + [@view] let cashPool ((), s : unit * storage) : nat = s.cashPool + [@view] let tezPool ((), s : unit * storage) : nat = s.tezPool + [@view] let storage ((), s: unit * storage) : storage = s + + +(* ============================================================================= + * Entrypoint Functions· + * ============================================================================= *) + +(* We assume the contract is originated with at least one liquidity + * provider set up already, so lqtTotal, cashPool and tezPool will + * always be positive after the initial setup, unless all liquidity is + * removed, at which point the contract is considered dead and stops working + * properly. If this is a concern, at least one address should keep at least a + * very small amount of liquidity in the contract forever. *) + +let add_liquidity (param : add_liquidity) (storage: storage) : result = + (* Adds liquidity to the contract, mints lqt in exchange for the deposited liquidity. *) + let { + owner = owner ; + minLqtMinted = minLqtMinted ; + maxCashDeposited = maxCashDeposited ; + deadline = deadline } = param in + let tezDeposited = mutez_to_natural (Tezos.get_amount ()) in + if (Tezos.get_now ()) >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else + (* The contract is initialized, use the existing exchange rate + mints nothing if the contract has been emptied, but that's OK *) + let tezPool : nat = storage.tezPool in + let lqt_minted : nat = tezDeposited * storage.lqtTotal / tezPool in + let cash_deposited : nat = ceildiv (tezDeposited * storage.cashPool) tezPool in + + if cash_deposited > maxCashDeposited then + (failwith error_MAX_CASH_DEPOSITED_MUST_BE_GREATER_THAN_OR_EQUAL_TO_CASH_DEPOSITED : result) + else if lqt_minted < minLqtMinted then + (failwith error_LQT_MINTED_MUST_BE_GREATER_THAN_MIN_LQT_MINTED : result) + else + let storage = {storage with + lqtTotal = storage.lqtTotal + lqt_minted ; + cashPool = storage.cashPool + cash_deposited ; + tezPool = storage.tezPool + tezDeposited} in + + (* send cash from sender to self *) + let op_cash = cash_transfer storage (Tezos.get_sender ()) (Tezos.get_self_address ()) cash_deposited in + (* mint lqt cash for them *) + let op_lqt = mint_or_burn storage owner (int lqt_minted) in + ([op_cash; op_lqt], storage) + +let remove_liquidity (param : remove_liquidity) (storage : storage) : result = + (* Removes liquidity to the contract by burning lqt. *) + let { to_ = to_ ; + lqtBurned = lqtBurned ; + minTezWithdrawn = minTezWithdrawn ; + minCashWithdrawn = minCashWithdrawn ; + deadline = deadline } = param in + + if (Tezos.get_now ()) >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else if (Tezos.get_amount ()) > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else begin + let tez_withdrawn : nat = (lqtBurned * storage.tezPool) / storage.lqtTotal in + let cash_withdrawn : nat = (lqtBurned * storage.cashPool) / storage.lqtTotal in + + (* Check that minimum withdrawal conditions are met *) + if tez_withdrawn < minTezWithdrawn then + (failwith error_THE_AMOUNT_OF_TEZ_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TEZ_WITHDRAWN : result) + else if cash_withdrawn < minCashWithdrawn then + (failwith error_THE_AMOUNT_OF_CASH_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_WITHDRAWN : result) + (* Proceed to form the operations and update the storage *) + else begin + (* calculate lqtTotal, convert int to nat *) + let new_lqtTotal = match (is_nat ( storage.lqtTotal - lqtBurned)) with + (* This check should be unecessary, the fa12 logic normally takes care of it *) + | None -> (failwith error_CANNOT_BURN_MORE_THAN_THE_TOTAL_AMOUNT_OF_LQT : nat) + | Some n -> n in + (* Calculate cashPool, convert int to nat *) + let new_cashPool = match is_nat (storage.cashPool - cash_withdrawn) with + | None -> (failwith error_CASH_POOL_MINUS_CASH_WITHDRAWN_IS_NEGATIVE : nat) + | Some n -> n in + let new_tezPool = match is_nat (storage.tezPool - tez_withdrawn) with + | None -> (failwith error_TEZ_POOL_MINUS_TEZ_WITHDRAWN_IS_NEGATIVE : nat) + | Some n -> n in + let op_lqt = mint_or_burn storage (Tezos.get_sender ()) (0 - lqtBurned) in + let op_cash = cash_transfer storage (Tezos.get_self_address ()) (Tezos.get_sender ()) cash_withdrawn in + let op_tez = tez_transfer to_ tez_withdrawn in + let storage = {storage with tezPool = new_tezPool ; lqtTotal = new_lqtTotal ; cashPool = new_cashPool} in + ([op_lqt; op_cash; op_tez], storage) + end + end + +let ctez_target ((target, minted): ctez_target) (storage : storage) = + if (Tezos.get_sender ()) <> storage.ctez_address then + (failwith error_CALLER_MUST_BE_CTEZ : result) + else + let updated_target = target in + let storage = {storage with target = updated_target ; cashPool = storage.cashPool + minted} in + (([] : operation list), storage) + + +let tez_to_cash (param : tez_to_cash) (storage : storage) = + let { to_ = to_ ; + minCashBought = minCashBought ; + deadline = deadline ; + rounds = rounds } = param in + let tezSold = mutez_to_natural (Tezos.get_amount ()) in + if (Tezos.get_now ()) >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else begin + (* We don't check that tezPool > 0, because that is impossible + unless all liquidity has been removed. *) + let tezPool = storage.tezPool in + let cash_bought = + // tez -> cash calculation; *includes a fee* + let bought = trade_dtez_for_dcash tezPool storage.cashPool tezSold storage.target rounds in + let bought_after_fee = bought * const_fee / const_fee_denom in + if bought_after_fee < minCashBought then + (failwith error_CASH_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_BOUGHT : nat) + else + bought_after_fee + in + let new_cashPool = (match is_nat (storage.cashPool - cash_bought) with + | None -> (failwith error_CASH_POOL_MINUS_CASH_BOUGHT_IS_NEGATIVE : nat) + | Some difference -> difference) in + + (* Update tezPool. *) + let storage = { storage with tezPool = storage.tezPool + tezSold ; cashPool = new_cashPool } in + (* Send tez from sender to self. *) + (* Send cash_withdrawn from exchange to sender. *) + let op_cash = cash_transfer storage (Tezos.get_self_address ()) to_ cash_bought in + ([op_cash], storage) + end + + +let cash_to_tez (param : cash_to_tez) (storage : storage) = + (* Accepts a payment in cash and sends tez. *) + let { to_ = to_ ; + cashSold = cashSold ; + minTezBought = minTezBought ; + deadline = deadline ; + rounds = rounds } = param in + + + if (Tezos.get_now ()) >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else if (Tezos.get_amount ()) > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else + (* We don't check that cashPool > 0, because that is impossible + unless all liquidity has been removed. *) + // cash -> tez calculation; *includes a fee* + let tez_bought = + let bought = trade_dcash_for_dtez storage.tezPool storage.cashPool cashSold storage.target rounds in + let bought_after_fee = bought * const_fee / const_fee_denom in + if bought_after_fee < minTezBought then + (failwith error_TEZ_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TEZ_BOUGHT : nat) + else + bought_after_fee + in + + let op_cash = cash_transfer storage (Tezos.get_sender ()) (Tezos.get_self_address ()) cashSold in + let op_tez = tez_transfer to_ tez_bought in + let new_tezPool = match is_nat (storage.tezPool - tez_bought) with + | None -> (failwith error_ASSERTION_VIOLATED_TEZ_BOUGHT_SHOULD_BE_LESS_THAN_TEZPOOL : nat) + | Some n -> n in + let storage = {storage with cashPool = storage.cashPool + cashSold ; + tezPool = new_tezPool} in + ([op_cash; op_tez], storage) + + +let default_ (storage : storage) : result = +(* Entrypoint to allow depositing tez. *) + (* update tezPool *) + let storage = {storage with tezPool = storage.tezPool + mutez_to_natural (Tezos.get_amount ())} in (([] : operation list), storage) + + +let set_lqt_address (lqtAddress : address) (storage : storage) : result = + if (Tezos.get_amount ()) > 0mutez then + (failwith error_AMOUNT_MUST_BE_ZERO : result) + else if storage.lqtAddress <> null_address then + (failwith error_LQT_ADDRESS_ALREADY_SET : result) + else + (([] : operation list), {storage with lqtAddress = lqtAddress}) + + +let tez_to_token (param : tez_to_token) (storage : storage) : result = + let { outputCfmmContract = outputCfmmContract ; + minTokensBought = minTokensBought ; + to_ = to_ ; + deadline = deadline ; + rounds = rounds } = param in + + let outputCfmmContract_contract: cash_to_token contract = + (match (Tezos.get_entrypoint_opt "%cashToToken" outputCfmmContract : cash_to_token contract option) with + | None -> (failwith error_INVALID_INTERMEDIATE_CONTRACT : cash_to_token contract) + | Some c -> c) in + + if (Tezos.get_now ()) >= deadline then + (failwith error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE : result) + else + let tezSold = mutez_to_natural (Tezos.get_amount ()) in + (* We don't check that cashPool > 0, because that is impossible unless all liquidity has been removed. *) + let cash_bought = + (let bought = trade_dtez_for_dcash storage.tezPool storage.cashPool tezSold storage.target rounds in + bought * const_fee / const_fee_denom) + in + let new_cashPool = match is_nat (storage.cashPool - cash_bought) with + | None -> (failwith error_CASH_POOL_MINUS_CASH_BOUGHT_IS_NEGATIVE : nat) + | Some n -> n in + let storage = {storage with tezPool = storage.tezPool + tezSold ; + cashPool = new_cashPool } in + let allow_output_to_withdraw_cash = + let cashContract_approve = (match (Tezos.get_entrypoint_opt "%approve" storage.cashAddress : (address * nat) contract option) with + | None -> (failwith error_MISSING_APPROVE_ENTRYPOINT_IN_CASH_CONTRACT : (address * nat) contract) + | Some c -> c) in + (Tezos.transaction (outputCfmmContract, 0n) + 0mutez + cashContract_approve, + Tezos.transaction (outputCfmmContract, cash_bought) + 0mutez + cashContract_approve) in + let op_send_cash_to_output = Tezos.transaction { minTokensBought = minTokensBought ; + cashSold = cash_bought ; + deadline = deadline ; to_ = to_} + 0mutez + outputCfmmContract_contract in + ([allow_output_to_withdraw_cash.0 ; allow_output_to_withdraw_cash.1 ; op_send_cash_to_output] , storage) + + +let update_consumer (operations, storage : result) : result = + if storage.lastOracleUpdate = (Tezos.get_now ()) + then (operations, storage) + else + let consumer = match (Tezos.get_contract_opt storage.consumerEntrypoint : (((nat * nat) * nat) contract) option) with + | None -> (failwith error_CANNOT_GET_CFMM_PRICE_ENTRYPOINT_FROM_CONSUMER : ((nat * nat) * nat) contract) + | Some c -> c in + ((Tezos.transaction ((marginal_price storage.tezPool storage.cashPool storage.target), storage.cashPool) 0mutez consumer) :: operations, + {storage with lastOracleUpdate = (Tezos.get_now ())}) + +let get_marginal_price (param : get_marginal_price) (storage : storage) : result = + let price = marginal_price storage.tezPool storage.cashPool storage.target in + let op = Tezos.transaction price 0mutez param in + ([op], storage) + +(* ============================================================================= + * Main + * ============================================================================= *) + +let main ((entrypoint, storage) : entrypoint * storage) : result = + match entrypoint with + | AddLiquidity param -> + add_liquidity param storage + | RemoveLiquidity param -> + remove_liquidity param storage + | CtezTarget param -> + ctez_target param storage + | TezToCash param -> + update_consumer + (tez_to_cash param storage) + | CashToTez param -> + update_consumer + (cash_to_tez param storage) + | TezToToken param -> + update_consumer + (tez_to_token param storage) + | SetLqtAddress param -> + set_lqt_address param storage + | GetMarginalPrice param -> + get_marginal_price param storage diff --git a/context.mligo b/context.mligo new file mode 100644 index 00000000..c32504c0 --- /dev/null +++ b/context.mligo @@ -0,0 +1,24 @@ +#include "stdctez.mligo" + +type t = + { target : nat + ; drift : int + ; _Q : nat (* Q is the desired quantity of ctez in the ctez half dex, + floor(Q * target) is the desired quantity of tez in the tez half dex *) + ; ctez_fa12_address : address + } + +let transfer_xtz (to_ : address) (amount : nat) : operation = + let contract = (Tezos.get_entrypoint "%default" to_ : unit contract) in + Tezos.transaction () (amount * 1mutez) contract + + +type fa12_transfer = + { [@annot:from] from_ : address + ; [@annot:to] to_ : address + ; value : nat + } + +let transfer_ctez (t : t) (from_ : address) (to_ : address) (value : nat) : operation = + let contract = (Tezos.get_entrypoint "%transfer" t.ctez_fa12_address : fa12_transfer contract) in + Tezos.transaction { from_; to_; value } 0mutez contract \ No newline at end of file diff --git a/ctez.mligo b/ctez.mligo index 9598d66d..c3d7e29c 100644 --- a/ctez.mligo +++ b/ctez.mligo @@ -25,22 +25,25 @@ type parameter = | Liquidate of liquidate | Register_deposit of register_deposit | Mint_or_burn of mint_or_burn - | Cfmm_price of nat * nat + | Cfmm_info of (nat * nat) * nat | Set_addresses of set_addresses | Get_target of nat contract -type oven = {tez_balance : tez ; ctez_outstanding : nat ; address : address} +type oven = {tez_balance : tez ; ctez_outstanding : nat ; address : address ; fee_index : nat} type storage = { ovens : (oven_handle, oven) big_map ; target : nat ; drift : int ; - last_drift_update : timestamp ; + last_update : timestamp ; ctez_fa12_address : address ; (* address of the fa12 contract managing the ctez token *) cfmm_address : address ; (* address of the cfmm providing the price feed *) + fee_index : nat ; } + type result = (operation list) * storage +(* Errors *) [@inline] let error_OVEN_ALREADY_EXISTS = 0n [@inline] let error_INVALID_CALLER_FOR_OVEN_OWNER = 1n @@ -56,35 +59,22 @@ type result = (operation list) * storage [@inline] let error_OVEN_NOT_UNDERCOLLATERALIZED = 11n [@inline] let error_EXCESSIVE_CTEZ_MINTING = 12n [@inline] let error_CALLER_MUST_BE_CFMM = 13n +[@inline] let error_INVALID_CTEZ_TARGET_ENTRYPOINT = 14n +[@inline] let error_IMPOSSIBLE = 999n (* an error that should never happen *) #include "oven.mligo" -let create (s : storage) (create : create) : result = - let handle = { id = create.id ; owner = Tezos.sender } in - if Big_map.mem handle s.ovens then - (failwith error_OVEN_ALREADY_EXISTS : result) - else - let (origination_op, oven_address) : operation * address = - create_oven create.delegate Tezos.amount { admin = Tezos.self_address ; handle = handle ; depositors = create.depositors } in - let oven = {tez_balance = Tezos.amount ; ctez_outstanding = 0n ; address = oven_address} in - let ovens = Big_map.update handle (Some oven) s.ovens in - ([origination_op], {s with ovens = ovens}) - -let set_addresses (s : storage) (addresses : set_addresses) : result = - if s.ctez_fa12_address <> ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) then - (failwith error_CTEZ_FA12_ADDRESS_ALREADY_SET : result) - else if s.cfmm_address <> ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) then - (failwith error_CFMM_ADDRESS_ALREADY_SET : result) - else - (([] : operation list), {s with ctez_fa12_address = addresses.ctez_fa12_address ; cfmm_address = addresses.cfmm_address}) +(* Functions *) let get_oven (handle : oven_handle) (s : storage) : oven = match Big_map.find_opt handle s.ovens with | None -> (failwith error_OVEN_DOESNT_EXIST : oven) - | Some o -> o + | Some oven -> (* Adjust the amount of outstanding ctez in the oven, record the fee index at that time. *) + let ctez_outstanding = abs((- oven.ctez_outstanding * s.fee_index) / oven.fee_index) in + {oven with fee_index = s.fee_index ; ctez_outstanding = ctez_outstanding} -let is_under_collateralized (oven : oven) (target : nat): bool = +let is_under_collateralized (oven : oven) (target : nat) : bool = (15n * oven.tez_balance) < (Bitwise.shift_right (oven.ctez_outstanding * target) 44n) * 1mutez let get_oven_withdraw (oven_address : address) : (tez * (unit contract)) contract = @@ -97,13 +87,45 @@ let get_oven_delegate (oven_address : address) : (key_hash option) contract = | None -> (failwith error_OVEN_MISSING_DELEGATE_ENTRYPOINT : (key_hash option) contract) | Some c -> c +let get_ctez_mint_or_burn (fa12_address : address) : (int * address) contract = + match (Tezos.get_entrypoint_opt "%mintOrBurn" fa12_address : ((int * address) contract) option) with + | None -> (failwith error_CTEZ_FA12_CONTRACT_MISSING_MINT_OR_BURN_ENTRYPOINT : (int * address) contract) + | Some c -> c + + +(* Views *) +[@view] let view_fee_index ((), s: unit * storage) : nat = s.fee_index +[@view] let view_target ((), s : unit * storage) : nat = s.target + +(* Entrypoint Functions *) +let create (s : storage) (create : create) : result = + let handle = { id = create.id ; owner = Tezos.get_sender () } in + if Big_map.mem handle s.ovens then + (failwith error_OVEN_ALREADY_EXISTS : result) + else + let (origination_op, oven_address) : operation * address = + create_oven create.delegate (Tezos.get_amount ()) { admin = Tezos.get_self_address () ; handle = handle ; depositors = create.depositors } in + let oven = {tez_balance = (Tezos.get_amount ()) ; ctez_outstanding = 0n ; address = oven_address ; fee_index = s.fee_index} in + let ovens = Big_map.update handle (Some oven) s.ovens in + ([origination_op], {s with ovens = ovens}) + +let set_addresses (s : storage) (addresses : set_addresses) : result = + if s.ctez_fa12_address <> ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) then + (failwith error_CTEZ_FA12_ADDRESS_ALREADY_SET : result) + else if s.cfmm_address <> ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) then + (failwith error_CFMM_ADDRESS_ALREADY_SET : result) + else + (([] : operation list), {s with ctez_fa12_address = addresses.ctez_fa12_address ; cfmm_address = addresses.cfmm_address}) + let withdraw (s : storage) (p : withdraw) : result = - let handle = {id = p.id ; owner = Tezos.sender} in + let handle = {id = p.id ; owner = Tezos.get_sender ()} in let oven : oven = get_oven handle s in let oven_contract = get_oven_withdraw oven.address in (* Check for undercollateralization *) - let new_balance = oven.tez_balance - p.amount in + let new_balance = match (oven.tez_balance - p.amount) with + | None -> (failwith error_EXCESSIVE_TEZ_WITHDRAWAL : tez) + | Some x -> x in let oven = {oven with tez_balance = new_balance} in let ovens = Big_map.update handle (Some oven) s.ovens in let s = {s with ovens = ovens} in @@ -115,7 +137,7 @@ let withdraw (s : storage) (p : withdraw) : result = let register_deposit (s : storage) (p : register_deposit) : result = (* First check that the call is legit *) let oven = get_oven p.handle s in - if oven.address <> Tezos.sender then + if oven.address <> Tezos.get_sender () then (failwith error_INVALID_CALLER_FOR_OVEN_OWNER : result) else (* register the increased balance *) @@ -123,36 +145,33 @@ let register_deposit (s : storage) (p : register_deposit) : result = let ovens = Big_map.update p.handle (Some oven) s.ovens in (([] : operation list), {s with ovens = ovens}) -let get_ctez_mint_or_burn (fa12_address : address) : (int * address) contract = - match (Tezos.get_entrypoint_opt "%mintOrBurn" fa12_address : ((int * address) contract) option) with - | None -> (failwith error_CTEZ_FA12_CONTRACT_MISSING_MINT_OR_BURN_ENTRYPOINT : (int * address) contract) - | Some c -> c - (* liquidate the oven by burning "quantity" ctez *) let liquidate (s: storage) (p : liquidate) : result = let oven : oven = get_oven p.handle s in if is_under_collateralized oven s.target then - let remaining_ctez = match Michelson.is_nat (oven.ctez_outstanding - p.quantity) with + let remaining_ctez = match is_nat (oven.ctez_outstanding - p.quantity) with | None -> (failwith error_CANNOT_BURN_MORE_THAN_OUTSTANDING_AMOUNT_OF_CTEZ : nat) | Some n -> n in (* get 32/31 of the target price, meaning there is a 1/31 penalty for the oven owner for being liquidated *) let extracted_balance = (Bitwise.shift_right (p.quantity * s.target) 43n) * 1mutez / 31n in (* 43 is 48 - log2(32) *) - let new_balance = oven.tez_balance - extracted_balance in + let new_balance = match oven.tez_balance - extracted_balance with + | None -> (failwith error_IMPOSSIBLE : tez) + | Some x -> x in let oven = {oven with ctez_outstanding = remaining_ctez ; tez_balance = new_balance} in let ovens = Big_map.update p.handle (Some oven) s.ovens in let s = {s with ovens = ovens} in let oven_contract = get_oven_withdraw oven.address in let op_take_collateral = Tezos.transaction (extracted_balance, p.to_) 0mutez oven_contract in let ctez_mint_or_burn = get_ctez_mint_or_burn s.ctez_fa12_address in - let op_burn_ctez = Tezos.transaction (-p.quantity, Tezos.sender) 0mutez ctez_mint_or_burn in + let op_burn_ctez = Tezos.transaction (-p.quantity, Tezos.get_sender ()) 0mutez ctez_mint_or_burn in ([op_burn_ctez ; op_take_collateral], s) else (failwith error_OVEN_NOT_UNDERCOLLATERALIZED : result) let mint_or_burn (s : storage) (p : mint_or_burn) : result = - let handle = { id = p.id ; owner = Tezos.sender } in + let handle = { id = p.id ; owner = Tezos.get_sender () } in let oven : oven = get_oven handle s in - let ctez_outstanding = match Michelson.is_nat (oven.ctez_outstanding + p.quantity) with + let ctez_outstanding = match is_nat (oven.ctez_outstanding + p.quantity) with | None -> (failwith error_CANNOT_BURN_MORE_THAN_OUTSTANDING_AMOUNT_OF_CTEZ : nat) | Some n -> n in let oven = {oven with ctez_outstanding = ctez_outstanding} in @@ -163,18 +182,18 @@ let mint_or_burn (s : storage) (p : mint_or_burn) : result = (* mint or burn quantity in the fa1.2 of ctez *) else let ctez_mint_or_burn = get_ctez_mint_or_burn s.ctez_fa12_address in - ([Tezos.transaction (p.quantity, Tezos.sender) 0mutez ctez_mint_or_burn], s) + ([Tezos.transaction (p.quantity, Tezos.get_sender ()) 0mutez ctez_mint_or_burn], s) let get_target (storage : storage) (callback : nat contract) : result = ([Tezos.transaction storage.target 0mutez callback], storage) -(* todo: restore when ligo interpret is fixed - let cfmm_price (storage : storage) (tez : tez) (token : nat) : result = *) -let cfmm_price (storage, tez, token : storage * nat * nat) : result = - if Tezos.sender <> storage.cfmm_address then + +let cfmm_info (storage : storage) (price_numerator : nat) (price_denominator : nat) (cash_pool : nat): result = + if Tezos.get_sender () <> storage.cfmm_address then (failwith error_CALLER_MUST_BE_CFMM : result) else - let delta = abs (Tezos.now - storage.last_drift_update) in + (* get the new target *) + let delta = abs (Tezos.get_now () - storage.last_update) in let target = storage.target in let d_target = Bitwise.shift_right (target * (abs storage.drift) * delta) 48n in (* We assume that `target - d_target < 0` never happens for economic reasons. @@ -188,20 +207,48 @@ let cfmm_price (storage, tez, token : storage * nat * nat) : result = for each day over or under the target by more than 1/64th. *) - let price = (Bitwise.shift_left tez 48n) / token in + let price : nat = (Bitwise.shift_left price_numerator 48n) / price_denominator in let target_less_price : int = target - price in let d_drift = let x = Bitwise.shift_left (abs (target_less_price * target_less_price)) 10n in let p2 = price * price in if x > p2 then delta else x * delta / p2 in + (* set new drift *) let drift = if target_less_price > 0 then storage.drift + d_drift else storage.drift - d_drift in - (([] : operation list), {storage with drift = drift ; last_drift_update = Tezos.now ; target = target}) + + (* Compute what the liquidity fee shoud be, based on the ratio of total outstanding ctez to ctez in cfmm *) + let outstanding = ( + match (Tezos.call_view "viewTotalSupply" () storage.ctez_fa12_address) with + | None -> (failwith unit : nat) + | Some n-> n + ) in + (* fee_r is given as a multiple of 2^(-48)... note that 2^(-32) Np / s ~ 0.73 cNp / year, so roughly a max of 0.73% / year *) + let fee_r = if 16n * cash_pool < outstanding then 65536n else if 8n * cash_pool > outstanding then 0n else + (Bitwise.shift_left (abs (outstanding - 8n * cash_pool)) 17n) / (outstanding) in + + let new_fee_index = storage.fee_index + Bitwise.shift_right (delta * storage.fee_index * fee_r) 48n in + (* Compute how many ctez have implicitly been minted since the last update *) + (* We round this down while we round the ctez owed up. This leads, over time, to slightly overestimating the outstanding ctez, which is conservative. *) + let minted = outstanding * (new_fee_index - storage.fee_index) / storage.fee_index in + + (* Create the operation to explicitly mint the ctez in the FA12 contract, and credit it to the CFMM *) + let ctez_mint_or_burn = get_ctez_mint_or_burn storage.ctez_fa12_address in + let op_mint_ctez = Tezos.transaction (minted, storage.cfmm_address) 0mutez ctez_mint_or_burn in + (* update the cfmm with a new target and mention its cashPool increase *) + + let cfmm_address = storage.cfmm_address in + let entrypoint_ctez_target = + (match (Tezos.get_entrypoint_opt "%ctezTarget" cfmm_address : (nat * nat) contract option) with + | None -> (failwith error_INVALID_CTEZ_TARGET_ENTRYPOINT : (nat * nat) contract) + | Some c -> c ) in + let op_ctez_target = Tezos.transaction (target, abs minted) 0tez entrypoint_ctez_target in + ([op_mint_ctez ; op_ctez_target], {storage with drift = drift ; last_update = Tezos.get_now () ; target = target; fee_index = new_fee_index}) let main (p, s : parameter * storage) : result = match p with @@ -210,7 +257,6 @@ let main (p, s : parameter * storage) : result = | Create d -> (create s d : result) | Liquidate l -> (liquidate s l : result) | Mint_or_burn xs -> (mint_or_burn s xs : result) - | Cfmm_price xs -> (cfmm_price (s, xs.0, xs.1) : result) + | Cfmm_info ((x,y),z) -> (cfmm_info s x y z : result) | Set_addresses xs -> (set_addresses s xs : result) | Get_target t -> (get_target s t : result) - diff --git a/ctez_2.mligo b/ctez_2.mligo new file mode 100644 index 00000000..003de52f --- /dev/null +++ b/ctez_2.mligo @@ -0,0 +1,263 @@ +#include "oven_types.mligo" +#include "stdctez.mligo" +#import "half_dex.mligo" Half_dex +#import "context.mligo" Context + +// TODO: Hook up half dex here + +type liquidate = + { handle : oven_handle + ; quantity : nat + ; [@annot:to] to_ : unit contract + } + +type mint_or_burn = + { id : nat + ; quantity : int + } + +type oven = + { tez_balance : tez + ; ctez_outstanding : nat + ; address : address + ; fee_index : nat + } + +type storage = + { ovens : (oven_handle, oven) big_map + ; last_update : timestamp + ; sell_ctez : Half_dex.t + ; sell_tez : Half_dex.t + ; context : Context.t + } + +(* Errors *) + +[@inline] let error_OVEN_ALREADY_EXISTS = 0n +[@inline] let error_INVALID_CALLER_FOR_OVEN_OWNER = 1n +[@inline] let error_CTEZ_FA12_ADDRESS_ALREADY_SET = 2n +[@inline] let error_CFMM_ADDRESS_ALREADY_SET = 3n +[@inline] let error_OVEN_DOESNT_EXIST= 4n +[@inline] let error_OVEN_MISSING_WITHDRAW_ENTRYPOINT = 5n +[@inline] let error_OVEN_MISSING_DEPOSIT_ENTRYPOINT = 6n +[@inline] let error_OVEN_MISSING_DELEGATE_ENTRYPOINT = 7n +[@inline] let error_EXCESSIVE_TEZ_WITHDRAWAL = 8n +[@inline] let error_CTEZ_FA12_CONTRACT_MISSING_MINT_OR_BURN_ENTRYPOINT = 9n +[@inline] let error_CANNOT_BURN_MORE_THAN_OUTSTANDING_AMOUNT_OF_CTEZ = 10n +[@inline] let error_OVEN_NOT_UNDERCOLLATERALIZED = 11n +[@inline] let error_EXCESSIVE_CTEZ_MINTING = 12n +[@inline] let error_CALLER_MUST_BE_CFMM = 13n +[@inline] let error_INVALID_CTEZ_TARGET_ENTRYPOINT = 14n +[@inline] let error_IMPOSSIBLE = 999n (* an error that should never happen *) + + +#include "oven.mligo" + +(* Functions *) + +let get_oven (handle : oven_handle) (s : storage) : oven = + match Big_map.find_opt handle s.ovens with + | None -> (failwith error_OVEN_DOESNT_EXIST : oven) + | Some oven -> + (* Adjust the amount of outstanding ctez in the oven, record the fee index at that time. *) + let new_fee_index = s.sell_ctez.fee_index * s.sell_tez.fee_index in + let ctez_outstanding = abs((- oven.ctez_outstanding * new_fee_index) / oven.fee_index) in + {oven with fee_index = new_fee_index ; ctez_outstanding = ctez_outstanding} + +let is_under_collateralized (oven : oven) (target : nat) : bool = + (15n * oven.tez_balance) < (Bitwise.shift_right (oven.ctez_outstanding * target) 44n) * 1mutez + +let get_oven_withdraw (oven_address : address) : (tez * (unit contract)) contract = + match (Tezos.get_entrypoint_opt "%oven_withdraw" oven_address : (tez * (unit contract)) contract option) with + | None -> (failwith error_OVEN_MISSING_WITHDRAW_ENTRYPOINT : (tez * (unit contract)) contract) + | Some c -> c + +let get_oven_delegate (oven_address : address) : (key_hash option) contract = + match (Tezos.get_entrypoint_opt "%oven_delegate" oven_address : (key_hash option) contract option) with + | None -> (failwith error_OVEN_MISSING_DELEGATE_ENTRYPOINT : (key_hash option) contract) + | Some c -> c + +let get_ctez_mint_or_burn (fa12_address : address) : (int * address) contract = + match (Tezos.get_entrypoint_opt "%mintOrBurn" fa12_address : ((int * address) contract) option) with + | None -> (failwith error_CTEZ_FA12_CONTRACT_MISSING_MINT_OR_BURN_ENTRYPOINT : (int * address) contract) + | Some c -> c + + +(* Views *) +[@view] let view_target ((), s : unit * storage) : nat = s.target + +type create_oven = {id : nat ; delegate : key_hash option ; depositors : depositors } + +(* Entrypoint Functions *) +[@entry] +let create_oven ({ id; delegate; depositors }: create_oven) (s : storage) : result = + let handle = { id ; owner = Tezos.get_sender () } in + if Big_map.mem handle s.ovens then + (failwith error_OVEN_ALREADY_EXISTS : result) + else + let (origination_op, oven_address) : operation * address = + originate_oven delegate (Tezos.get_amount ()) { admin = Tezos.get_self_address () ; handle = handle ; depositors = depositors } in + let oven = {tez_balance = (Tezos.get_amount ()) ; ctez_outstanding = 0n ; address = oven_address ; fee_index = s.sell_ctez.fee_index * s.sell_tez.fee_index} in + let ovens = Big_map.update handle (Some oven) s.ovens in + ([origination_op], {s with ovens = ovens}) + +// called on initialization to set the ctez_fa12_address +[@entry] +let set_ctez_fa12_address (ctez_fa12_address : address) (s : storage) : result = + if s.ctez_fa12_address <> ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) then + (failwith error_CTEZ_FA12_ADDRESS_ALREADY_SET : result) + else + (([] : operation list), {s with ctez_fa12_address = ctez_fa12_address}) + +type withdraw = { id : nat ; amount : tez ; [@annot:to] to_ : unit contract } + +[@entry] +let withdraw_from_oven (p : withdraw) (s : storage) : result = + let handle = {id = p.id ; owner = Tezos.get_sender ()} in + let oven : oven = get_oven handle s in + let oven_contract = get_oven_withdraw oven.address in + + (* Check for undercollateralization *) + let new_balance = match (oven.tez_balance - p.amount) with + | None -> (failwith error_EXCESSIVE_TEZ_WITHDRAWAL : tez) + | Some x -> x in + let oven = {oven with tez_balance = new_balance} in + let ovens = Big_map.update handle (Some oven) s.ovens in + let s = {s with ovens = ovens} in + if is_under_collateralized oven s.target then + (failwith error_EXCESSIVE_TEZ_WITHDRAWAL : result) + else + ([Tezos.transaction (p.amount, p.to_) 0mutez oven_contract], s) + + +[@entry] +let register_oven_deposit (p : register_oven_deposit) (s : storage) : result = + (* First check that the call is legit *) + let oven = get_oven p.handle s in + if oven.address <> Tezos.get_sender () then + (failwith error_INVALID_CALLER_FOR_OVEN_OWNER : result) + else + (* register the increased balance *) + let oven = {oven with tez_balance = oven.tez_balance + p.amount} in + let ovens = Big_map.update p.handle (Some oven) s.ovens in + (([] : operation list), {s with ovens = ovens}) + +(* liquidate the oven by burning "quantity" ctez *) +[@entry] +let liquidate_oven (p : liquidate) (s: storage) : result = + let oven : oven = get_oven p.handle s in + if is_under_collateralized oven s.target then + let remaining_ctez = match is_nat (oven.ctez_outstanding - p.quantity) with + | None -> (failwith error_CANNOT_BURN_MORE_THAN_OUTSTANDING_AMOUNT_OF_CTEZ : nat) + | Some n -> n in + (* get 32/31 of the target price, meaning there is a 1/31 penalty for the oven owner for being liquidated *) + let extracted_balance = (Bitwise.shift_right (p.quantity * s.target) 43n) * 1mutez / 31n in (* 43 is 48 - log2(32) *) + let new_balance = match oven.tez_balance - extracted_balance with + | None -> (failwith error_IMPOSSIBLE : tez) + | Some x -> x in + let oven = {oven with ctez_outstanding = remaining_ctez ; tez_balance = new_balance} in + let ovens = Big_map.update p.handle (Some oven) s.ovens in + let s = {s with ovens = ovens} in + let oven_contract = get_oven_withdraw oven.address in + let op_take_collateral = Tezos.transaction (extracted_balance, p.to_) 0mutez oven_contract in + let ctez_mint_or_burn = get_ctez_mint_or_burn s.ctez_fa12_address in + let op_burn_ctez = Tezos.transaction (-p.quantity, Tezos.get_sender ()) 0mutez ctez_mint_or_burn in + ([op_burn_ctez ; op_take_collateral], s) + else + (failwith error_OVEN_NOT_UNDERCOLLATERALIZED : result) + +[@entry] +let mint_or_burn (p : mint_or_burn) (s : storage) : result = + let handle = { id = p.id ; owner = Tezos.get_sender () } in + let oven : oven = get_oven handle s in + let ctez_outstanding = match is_nat (oven.ctez_outstanding + p.quantity) with + | None -> (failwith error_CANNOT_BURN_MORE_THAN_OUTSTANDING_AMOUNT_OF_CTEZ : nat) + | Some n -> n in + let oven = {oven with ctez_outstanding = ctez_outstanding} in + let ovens = Big_map.update handle (Some oven) s.ovens in + let s = {s with ovens = ovens} in + if is_under_collateralized oven s.target then + (failwith error_EXCESSIVE_CTEZ_MINTING : result) + (* mint or burn quantity in the fa1.2 of ctez *) + else + let ctez_mint_or_burn = get_ctez_mint_or_burn s.ctez_fa12_address in + ([Tezos.transaction (p.quantity, Tezos.get_sender ()) 0mutez ctez_mint_or_burn], s) + +[@view] +let get_target () (storage : storage) : nat = storage.target + +[@view] +let get_drift () (storage : storage) : int = storage.drift + +let min (a : nat) b = if a < b then a else b + +[@inline] +let drift_adjustment (storage : storage) : int = + let target = storage.target in + let qc = min storage.sell_ctez.total_liquidity storage._Q in + let qt = min storage.sell_tez.total_liquidity (storage._Q * target) in + let tqc_m_qt = target * qc - qt in + let tQ = target * storage._Q in + tqc_m_qt * tqc_m_qt * tqc_m_qt / (tQ * tQ * tQ) + + +let fee_rate (_Q : nat) (q : nat) : nat = + if 16n * q < _Q then 65536n else if 16n * q > 15n * _Q then 0n else + abs (15 - 14 * q / _Q) * 65536n / 14n + + +let clamp_nat (x : int) : nat = + match is_nat x with + | None -> 0n + | Some x -> x + +let update_fee_index (ctez_fa12_address: address) (delta: nat) (outstanding : nat) (_Q : nat) (dex : half_dex) : half_dex * nat * operation = + let rate = fee_rate _Q dex.total_liquidity in + (* rate is given as a multiple of 2^(-48)... note that 2^(-32) Np / s ~ 0.73 cNp / year, so roughly a max of 0.73% / year *) + let new_fee_index = dex.fee_index + Bitwise.shift_right (delta * dex.fee_index * rate) 48n in + (* Compute how many ctez have implicitly been minted since the last update *) + (* We round this down while we round the ctez owed up. This leads, over time, to slightly overestimating the outstanding ctez, which is conservative. *) + let minted = outstanding * (new_fee_index - dex.fee_index) / dex.fee_index in + + (* Create the operation to explicitly mint the ctez in the FA12 contract, and credit it to the CFMM *) + let ctez_mint_or_burn = get_ctez_mint_or_burn ctez_fa12_address in + let op_mint_ctez = Tezos.transaction (minted, Tezos.get_self_address ()) 0mutez ctez_mint_or_burn in + + {dex with fee_index = new_fee_index; total_subsidy = clamp_nat (dex.total_subsidy + minted) }, clamp_nat (outstanding + minted), op_mint_ctez + + +let housekeeping () (storage : storage) : result = + let curr_timestamp = Tezos.get_now () in + if storage.last_update <> curr_timestamp then + let d_drift = drift_adjustment storage in + (* This is not homegeneous, but setting the constant delta is multiplied with + to 1.0 magically happens to be reasonable. Why? + Because (24 * 3600 / 2^48) * 365.25*24*3600 ~ 0.97%. + This means that the annualized drift changes by roughly one percentage point per day at most. + *) + let new_drift = storage.drift + d_drift in + + let delta = abs (curr_timestamp - storage.last_update) in + let target = storage.target in + let d_target = Bitwise.shift_right (target * (abs storage.drift) * delta) 48n in + (* We assume that `target - d_target < 0` never happens for economic reasons. + Concretely, even drift were as low as -50% annualized, it would take not + updating the target for 1.4 years for a negative number to occur *) + let new_target = if storage.drift < 0 then abs (target - d_target) else target + d_target in + (* Compute what the liquidity fee shoud be, based on the ratio of total outstanding ctez to ctez in dexes *) + let outstanding = ( + match (Tezos.call_view "viewTotalSupply" () storage.ctez_fa12_address) with + | None -> (failwith unit : nat) + | Some n-> n + ) in + let storage = { storage with _Q = outstanding / 20n } in + let sell_ctez, outstanding, op_mint_ctez1 = update_fee_index storage.ctez_fa12_address delta outstanding storage._Q storage.sell_ctez in + let sell_tez, _outstanding, op_mint_ctez2 = update_fee_index storage.ctez_fa12_address delta outstanding (storage._Q * storage.target) storage.sell_tez in + let storage = { storage with sell_ctez = sell_ctez ; sell_tez = sell_tez } in + + ([op_mint_ctez1 ; op_mint_ctez2], {storage with drift = new_drift ; last_update = curr_timestamp ; target = new_target }) + else + ([], storage) + + + diff --git a/ctez_initial_storage.ml b/ctez_initial_storage.ml new file mode 100644 index 00000000..eb17714c --- /dev/null +++ b/ctez_initial_storage.ml @@ -0,0 +1,7 @@ +{ + ovens = (Big_map.empty : (oven_handle, oven) big_map) ; + target = Bitwise.shift_left 1n 48n ; drift = 0 ; + last_drift_update = ("DEPLOYMENT_DATET00:00:00Z" : timestamp) ; + ctez_fa12_address = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) ; + cfmm_address = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) ; +} diff --git a/ctez_initial_storage.mligo b/ctez_initial_storage.mligo index eb17714c..d2cd5c69 100644 --- a/ctez_initial_storage.mligo +++ b/ctez_initial_storage.mligo @@ -1,7 +1,8 @@ { ovens = (Big_map.empty : (oven_handle, oven) big_map) ; target = Bitwise.shift_left 1n 48n ; drift = 0 ; - last_drift_update = ("DEPLOYMENT_DATET00:00:00Z" : timestamp) ; + last_update = ("DEPLOYMENT_DATET00:00:00Z" : timestamp) ; ctez_fa12_address = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) ; cfmm_address = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) ; + fee_index = 281474976710656n ; (*2^48*) } diff --git a/description.md b/description.md new file mode 100644 index 00000000..d20066e4 --- /dev/null +++ b/description.md @@ -0,0 +1,83 @@ +# A description of ctez mechanisms + +This is a rough outline of how ctez works in practice, with a few worked out examples. + +## Motivation + +The concept of delegation on Tezos can render certain smart-contracts tricky, especially when assets from multiple participants are pooled together. A wrapped version of tez is useful in those situations, if we can achieve the following properties: + +1. fungibility: those tokens must be interchangeable with each other +2. decentralization and trust minimization: they should not benefit any specific baker, nor leave that option open to any governance body +3. no opportunity cost from missing out on delegation + +It's fairly easy to satisfy two out of three constraints. If we do not care about missing out on delegation, a simple contracts that wraps tez and does not delegate the tez it holds will do the job. If we do not care about fungibility, we can let everyone create their own wrapped token backed by funds they delegated. Lastly, if we do not care about decentralization or trustlessness, we could leave the decision of where to delegate pooled funds to some third party. + +Ctez can achieve all three property with a bit of mathematics! + + ## Glossary + +In order to explain and understand ctez, it first helps to define a precise glossary. + +### Target + +The target represents the target peg between tez and ctez. A target of 1.5 means that the target peg is 1 ctez = 1.5 tez. Inasmuch as the ctez contract "tries" to do anything, it tries to automatically balance economic forces so that ctez is generally exchanged for tez at or around the target peg. The peg is only a target, there is no hard guarantee that, at any given time, ctez can be exchanged for tez at the target peg or vice-versa. + +The target peg starts out at 1.0, meaning the smart-contracts initially attempts to balance out incentives so that 1 tez is exchangeable for 1 ctez, but this changes over time. **This is important**, the target peg between tez and ctez is not fixed, it changes over time. It changes smoothly, predictably, and relatively slowly, but it changes. All else equal, at equilibrium this change may tend to compensate for the opportunity cost of delegation + +### Drift + +**This is the most important number to pay attention to** in ctez, it represents how quickly the target peg between ctez and tez increases or decreases. If the drift is positive, then the target peg is going to increase, if the drift is negative, the target peg is going to decrease. + +### Premium + +The premium is **the second most important number to pay attention to** in ctez. It represents the ratio between the price of ctez in tez, as observed in one specific cfmm contract (a so-called "dex"), and the target. If the premium is negative, then ctez is below its target, if the premium is positive, then it is above its target. + +### Oven + +A smart contract holding tez, which lets its owner mint new ctez. The number of tez in the contract must exceed the number of ctez minted times the target peg, times 16/15. The last factor represents a safety buffer of 6.667%. The total number of ctez minted by an oven is called the outstanding amount of ctez. If ctez is returned to the oven (burned), the outstanding amount decreases (as does the total supply of ctez). + +### Liquidation + +If the drift is positive, the target increases over time. When the target increases, it's possible that some ovens end up having more ctez outstanding than they were allowed to mint in the first place. When that happens, anyone can come in and return ctez to the oven, in exchange for tez. The exchange happens **at the target peg** plus 3.226% (1/31), not at the market price. The entirety of the oven can be liquidated, not just the amount necessary to make it properly collateralized again. Ovens owner can avert liquidation and the associated penalty by returning ctez to the oven ahead of time. Liquidations should never come as a surprise if the oven owners are paying attention. + + +## Economic mechanisms + +So how does ctez maintain, or tries to maintain, a peg close to the target, anyway? When the premium is negative, the drift *increases*. This can seem surprising, if the price is below the target, shouldn't we try to make the target smaller to meet the price by decreasing the drift or even making the drift negative? No, this would just lead to a spiral where it ends up at 0. + +Assume for the moment that ctez generally succeeds in eventually restoring its peg. It's a very big and very important "if", but it will be motivated below. *If* that is the case, a higher drift makes it more attractive to hold ctez and less attractive to have outstanding ctez in an oven, meaning it leads people to buy ctez or to return it to ovens reducing the supply of ctez, which tends to moves the price in the direction of the peg. + +Likewise, if the premium is positive, decreasing the drift makes it less attractive to hold ctez and more attractive to mint it and sell it, which puts downwards pressure on the price of ctez in tez. + +### What if a discount persists for a long time? + +Let's motivate that important "if". Why do we assume that the peg can be eventually restored? So long as ctez trades at a discount, the drift keeps increasing. This means that the target increases more than exponentially, it increases like the exponential of a quadratic function of time! + +To give some concrete numbers, suppose the target starts at 1.0, that the drift starts at 0% per year, and that a premium of at last -3.125% (a discount) persists continuously for a very long time. After one day, the drift will be about 1% per year, after two days, it will be about 2% per year. How does this look for the target over the course of one year? + +It looks like this. +![](https://i.imgur.com/bms5TZo.png) + +After one month, the target would be about `1.012`, after 2 months `1.05`, after 3 months, `1.12`, after 6 months, `1.56`, after a year, `5.85`. + +Still, what prevents everyone from blissfully ignoring the target? In one word: liquidations. Oven owners cannot (and should not) ignore a rising target forever. They cannot outpace the target for very long and, at some point, they will have to either close their oven or be liquidated and, when that happens, it will be at the target price. So ctez holders may not always be able to redeem at the target, but they can wait for the discount to disappear. They could wait for months, but the longer they are forced to wait, the more attractive it becomes. + +If oven owners decided to be very conservative and only mint 1% of the maximum amount of ctez they can mint, it could take 19 to 20 months for the target to rise enough to liquidate them — that's a long time — but at this point the target would be over 100 and liquidation would happen at that price. + +### What if a positive premium persists for a long time + +If a positive premium persists for a long time, then the drift will start going down, and might even become negative. At this point, oven owners could simply mint ctez, sell them for tez, use those to mint more ctez and not have to worry about liquidation because the target is decreasing. If the premium becomes negative, then they can buy back ctez and unwide the position. Even if liquidity is limited, the mere unwinding of their position can prevent the drift from rising until they are fully unwound. + +### A healthy drift + +Of course, the normal state of affair is not one where discounts last for two years and allow ctez to be exchanged at 100x in liquidations. Ctez holders and oven owners can and should react to the drift. As a rule of thumb, the drift should be around the baking reward (about 5.5% as these lines are written). If the drift is higher than this, then it can be more attractive to hold ctez than to hold and delegate tez. If the drift is lower than this, then oven owners can mint ctez and increase their balance via baking rewards faster then the target increases. + +A simple rule for oven owners or ctez holders is to pick the drift they are comfortable with, and to increase or decrease their position depending on what the drift is. + +## How does the drift change exactly? + +The exact formula for the change of the drift is detailed in the github repo for ctez but, overall, when the premium or discount is large (more than 1/32 or 3.125%), then the drift can change by as much as 1%/year every day. When it's in the middle, if follows a quadratic formula. The graph looks like this. + +![](https://i.imgur.com/LhDAEuL.png) + +Where the y axis represents (approximately) how many percentage points per year are added or removed to the drift on a daily basis, and the y axis represents the premium. As we can see, when the premium or discount is small, the drift changes very little. The drift adjustment for a premium or discount of 0.5% is about 40 times smaller than the drift adjustment for a premium or discount of 3.125%. \ No newline at end of file diff --git a/errors.mligo b/errors.mligo new file mode 100644 index 00000000..62773065 --- /dev/null +++ b/errors.mligo @@ -0,0 +1,42 @@ +(* ============================================================================= + * Error codes + * ============================================================================= *) + +[@inline] let error_TOKEN_CONTRACT_MUST_HAVE_A_TRANSFER_ENTRYPOINT = 0n +[@inline] let error_ASSERTION_VIOLATED_CASH_BOUGHT_SHOULD_BE_LESS_THAN_CASHPOOL = 1n +[@inline] let error_PENDING_POOL_UPDATES_MUST_BE_ZERO = 2n +[@inline] let error_THE_CURRENT_TIME_MUST_BE_LESS_THAN_THE_DEADLINE = 3n +[@inline] let error_MAX_TOKENS_DEPOSITED_MUST_BE_GREATER_THAN_OR_EQUAL_TO_TOKENS_DEPOSITED = 4n +[@inline] let error_LQT_MINTED_MUST_BE_GREATER_THAN_MIN_LQT_MINTED = 5n +[@inline] let error_MAX_CASH_DEPOSITED_MUST_BE_GREATER_THAN_OR_EQUAL_TO_CASH_DEPOSITED = 6n +[@inline] let error_ONLY_NEW_MANAGER_CAN_ACCEPT = 7n +[@inline] let error_CASH_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_BOUGHT = 8n +[@inline] let error_INVALID_TO_ADDRESS = 9n +[@inline] let error_AMOUNT_MUST_BE_ZERO = 10n +[@inline] let error_THE_AMOUNT_OF_CASH_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_CASH_WITHDRAWN = 11n +[@inline] let error_LQT_CONTRACT_MUST_HAVE_A_MINT_OR_BURN_ENTRYPOINT = 12n +[@inline] let error_THE_AMOUNT_OF_TOKENS_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_WITHDRAWN = 13n +[@inline] let error_CANNOT_BURN_MORE_THAN_THE_TOTAL_AMOUNT_OF_LQT = 14n +[@inline] let error_TOKEN_POOL_MINUS_TOKENS_WITHDRAWN_IS_NEGATIVE = 15n +[@inline] let error_CASH_POOL_MINUS_CASH_WITHDRAWN_IS_NEGATIVE = 16n +[@inline] let error_CASH_POOL_MINUS_CASH_BOUGHT_IS_NEGATIVE = 17n +[@inline] let error_TOKENS_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TOKENS_BOUGHT = 18n +[@inline] let error_TOKEN_POOL_MINUS_TOKENS_BOUGHT_IS_NEGATIVE = 19n +[@inline] let error_ONLY_MANAGER_CAN_SET_BAKER = 20n +[@inline] let error_ONLY_MANAGER_CAN_SET_MANAGER = 21n +[@inline] let error_BAKER_PERMANENTLY_FROZEN = 22n +[@inline] let error_LQT_ADDRESS_ALREADY_SET = 24n +[@inline] let error_CALL_NOT_FROM_AN_IMPLICIT_ACCOUNT = 25n +[@inline] let error_CASH_CONTRACT_MUST_HAVE_A_TRANSFER_ENTRYPOINT = 26n +[@inline] let error_TEZ_POOL_MINUS_TEZ_WITHDRAWN_IS_NEGATIVE = 27n +[@inline] let error_INVALID_FA12_TOKEN_CONTRACT_MISSING_GETBALANCE = 28n +[@inline] let error_THIS_ENTRYPOINT_MAY_ONLY_BE_CALLED_BY_GETBALANCE_OF_TOKENADDRESS = 29n +[@inline] let error_INVALID_FA2_BALANCE_RESPONSE = 30n +[@inline] let error_INVALID_INTERMEDIATE_CONTRACT = 31n +[@inline] let error_THIS_ENTRYPOINT_MAY_ONLY_BE_CALLED_BY_GETBALANCE_OF_CASHADDRESS = 30n +[@inline] let error_TEZ_DEPOSIT_WOULD_BE_BURNED = 32n +[@inline] let error_INVALID_FA12_CASH_CONTRACT_MISSING_GETBALANCE = 33n +[@inline] let error_MISSING_APPROVE_ENTRYPOINT_IN_CASH_CONTRACT = 34n +[@inline] let error_CANNOT_GET_CFMM_PRICE_ENTRYPOINT_FROM_CONSUMER = 35n +[@inline] let error_THE_AMOUNT_OF_TEZ_WITHDRAWN_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TEZ_WITHDRAWN = 36n +[@inline] let error_TEZ_BOUGHT_MUST_BE_GREATER_THAN_OR_EQUAL_TO_MIN_TEZ_BOUGHT = 37n diff --git a/fa12.mligo b/fa12.mligo index 7c2fa501..1aca3ce8 100644 --- a/fa12.mligo +++ b/fa12.mligo @@ -65,10 +65,10 @@ let transfer (param : transfer) (storage : storage) : result = let allowances = storage.allowances in let tokens = storage.tokens in let allowances = - if Tezos.sender = param.address_from + if Tezos.get_sender () = param.address_from then allowances else - let allowance_key = { owner = param.address_from ; spender = Tezos.sender } in + let allowance_key = { owner = param.address_from ; spender = Tezos.get_sender () } in let authorized_value = match Big_map.find_opt allowance_key allowances with | Some value -> value @@ -99,7 +99,7 @@ let transfer (param : transfer) (storage : storage) : result = let approve (param : approve) (storage : storage) : result = let allowances = storage.allowances in - let allowance_key = { owner = Tezos.sender ; spender = param.spender } in + let allowance_key = { owner = Tezos.get_sender (); spender = param.spender } in let previous_value = match Big_map.find_opt allowance_key allowances with | Some value -> value @@ -114,7 +114,7 @@ let approve (param : approve) (storage : storage) : result = let mintOrBurn (param : mintOrBurn) (storage : storage) : result = begin - if Tezos.sender <> storage.admin + if Tezos.get_sender () <> storage.admin then failwith "OnlyAdmin" else (); let tokens = storage.tokens in @@ -149,9 +149,21 @@ let getTotalSupply (param : getTotalSupply) (storage : storage) : operation list let total = storage.total_supply in [Tezos.transaction total 0mutez param.callback] + +[@view] let viewBalanceOption (owner, s : address * storage) : nat option = + Big_map.find_opt owner s.tokens + +[@view] let viewBalance (owner, s : address * storage) : nat = + match Big_map.find_opt owner s.tokens with + | Some value -> value + | None -> 0n + +[@view] let viewTotalSupply ((),s : unit * storage) : nat = + s.total_supply + let main (param, storage : parameter * storage) : result = begin - if Tezos.amount <> 0mutez + if Tezos.get_amount () <> 0mutez then failwith "DontSendTez" else (); match param with diff --git a/fa12_ctez_initial_storage.ml b/fa12_ctez_initial_storage.ml new file mode 100644 index 00000000..6c08e9e8 --- /dev/null +++ b/fa12_ctez_initial_storage.ml @@ -0,0 +1,7 @@ +{ + tokens = ( Big_map.literal [("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address), 1n] : + (address, nat) big_map) ; + allowances = (Big_map.empty : allowances) ; + admin = ("ADMIN_ADDRESS" : address) ; + total_supply = 1n +} diff --git a/fees.md b/fees.md new file mode 100644 index 00000000..4287ca86 --- /dev/null +++ b/fees.md @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/frontend/app/.env b/frontend/app/.env index 342f29ce..9cf06411 100644 --- a/frontend/app/.env +++ b/frontend/app/.env @@ -1,11 +1,11 @@ REACT_APP_APP_NAME=CTez -REACT_APP_CTEZ_CONTRACT=KT1JVJLCqFBvcN8RwBAfQy56UZVw5E4THdjo -REACT_APP_CFMM_CONTRACT=KT1T9gMqiXsmTXADrwFcruRBYRXCnYsqbM9i -REACT_APP_CTEZ_FA12_CONTRACT=KT1DKZZbMyFeXmZJT729tWPxN5i1kChX8obw -REACT_APP_LQT_FA12_CONTRACT=KT1Moyw9naQDXX2oPd7EVh4ckEmwKfKNumwe -REACT_APP_NETWORK_TYPE=florencenet -REACT_APP_RPC_URL=https://florencenet.smartpy.io +REACT_APP_CTEZ_CONTRACT=KT1KmgDGPwkMszJa72bmaVuKCfqw4kPTowWy +REACT_APP_CFMM_CONTRACT=KT1Wmj5xaufYqY2x9HMotmydMgDw5jfG4fuq +REACT_APP_CTEZ_FA12_CONTRACT=KT1QBSXZeKrjF5LJTn2PGyySYNLCWPbN4ePA +REACT_APP_LQT_FA12_CONTRACT=KT1MRDopKKEFRb9DCaRb59ePCqfoELjtVXu6 +REACT_APP_NETWORK_TYPE=granadanet +REACT_APP_RPC_URL=https://granadanet.smartpy.io REACT_APP_RPC_PORT=443 -REACT_APP_TZKT=https://api.florencenet.tzkt.io +REACT_APP_TZKT=https://api.granadanet.tzkt.io REACT_APP_TZKT_PORT=443 -REACT_APP_CONTRACT_DEPLOYMENT_DATE=2021-05-31 \ No newline at end of file +REACT_APP_CONTRACT_DEPLOYMENT_DATE=2021-09-14 \ No newline at end of file diff --git a/frontend/app/.eslintrc.js b/frontend/app/.eslintrc.js index 16246898..73a64c4f 100644 --- a/frontend/app/.eslintrc.js +++ b/frontend/app/.eslintrc.js @@ -61,5 +61,6 @@ module.exports = { 'no-nested-ternary': 'warn', 'no-unneeded-ternary': 'warn', '@typescript-eslint/naming-convention': 'warn', + 'no-bitwise': 'off', }, }; diff --git a/frontend/app/package.json b/frontend/app/package.json index 9119cb3e..b869accc 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -11,8 +11,8 @@ "@material-ui/icons": "^5.0.0-alpha.26", "@material-ui/pickers": "4.0.0-alpha.12", "@reduxjs/toolkit": "^1.5.1", - "@taquito/beacon-wallet": "^9.0.1", - "@taquito/taquito": "^9.0.1", + "@taquito/beacon-wallet": "^10.1.1", + "@taquito/taquito": "^10.1.1", "axios": "^0.21.1", "bignumber.js": "^9.0.1", "blockies-ts": "^1.0.0", diff --git a/frontend/app/src/api/contracts.ts b/frontend/app/src/api/contracts.ts index 85c7d82a..ef9b71ad 100644 --- a/frontend/app/src/api/contracts.ts +++ b/frontend/app/src/api/contracts.ts @@ -3,6 +3,7 @@ import { sub, format, differenceInDays } from 'date-fns'; import { getCfmmStorage, getLQTContractStorage } from '../contracts/cfmm'; import { getCtezStorage } from '../contracts/ctez'; import { BaseStats, CTezTzktStorage, UserLQTData } from '../interfaces'; +import { calculateMarginalPrice } from '../utils/cfmmUtils'; import { CONTRACT_DEPLOYMENT_DATE } from '../utils/globals'; import { getCTezTzktStorage, getLastBlockOfTheDay } from './tzkt'; @@ -24,7 +25,11 @@ export const getBaseStats = async (userAddress?: string): Promise => const cTez7dayStorage = await getPrevCTezStorage(prevStorageDays, userAddress); const prevTarget = Number(cTez7dayStorage.target) / 2 ** 48; const currentTarget = cTezStorage.target.toNumber() / 2 ** 48; - const currentPrice = cfmmStorage.cashPool.toNumber() / cfmmStorage.tokenPool.toNumber(); + const currentPrice = calculateMarginalPrice( + cfmmStorage.tezPool.toNumber(), + cfmmStorage.cashPool.toNumber(), + cTezStorage.target.toNumber(), + ); const premium = currentPrice === currentTarget ? 0 : currentPrice / currentTarget - 1.0; const drift = cTezStorage.drift.toNumber(); const currentAnnualDrift = (1.0 + drift / 2 ** 48) ** (365.25 * 24 * 3600) - 1.0; @@ -54,18 +59,3 @@ export const getUserLQTData = async (userAddress: string): Promise ), }; }; - -export const isMonthFromLiquidation = ( - outstandingCtez: number, - target: number, - tezBalance: number, - currentDrift: number, -): boolean => { - return ( - outstandingCtez * - (target / 2 ** 48) * - (1 + currentDrift / 2 ** 48) ** ((365.25 * 24 * 3600) / 12) * - (16 / 15) > - tezBalance - ); -}; diff --git a/frontend/app/src/components/OvenActions/MintOrBurn.tsx b/frontend/app/src/components/OvenActions/MintOrBurn.tsx index 20f2928c..739f95b5 100644 --- a/frontend/app/src/components/OvenActions/MintOrBurn.tsx +++ b/frontend/app/src/components/OvenActions/MintOrBurn.tsx @@ -13,7 +13,7 @@ import { CTezIcon } from '../CTezIcon/CTezIcon'; import { getOvenMaxCtez } from '../../utils/ovenUtils'; import Typography from '../Typography'; import { logger } from '../../utils/logger'; -import { isMonthFromLiquidation } from '../../api/contracts'; +import { isMonthFromLiquidation } from '../../utils/cfmmUtils'; interface MintOrBurnProps { type: 'mint' | 'repay'; diff --git a/frontend/app/src/components/OvenCard/OvenCard.tsx b/frontend/app/src/components/OvenCard/OvenCard.tsx index 56160359..70f2abcb 100644 --- a/frontend/app/src/components/OvenCard/OvenCard.tsx +++ b/frontend/app/src/components/OvenCard/OvenCard.tsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from '@emotion/styled'; -import { Box, Button, Chip, Grid } from '@material-ui/core'; +import { Avatar, Box, Button, Chip, Grid, Skeleton } from '@material-ui/core'; import { FcImport, FcExport } from 'react-icons/fc'; import Card from '@material-ui/core/Card'; import CardHeader from '@material-ui/core/CardHeader'; @@ -22,6 +22,7 @@ interface OvenCardProps extends Oven { isImported?: boolean; action?: () => void | Promise; removeExternalAction?: () => void | Promise; + isLoading?: boolean; } export const StyledCard = styled(Card)` @@ -53,6 +54,7 @@ const OvenCardComponent: React.FC = ({ isExternal = false, isImported = false, removeExternalAction, + isLoading, }) => { const { t } = useTranslation(['common']); const maxMintableCtez = maxCtez < 0 ? 0 : maxCtez; @@ -62,16 +64,28 @@ const OvenCardComponent: React.FC = ({ - - + isLoading ? ( + + + + ) : ( + + + + ) + } + title={ + isLoading ? ( + + ) : ( +
+ ) } - title={
} subheader={ - {`${t('ovenBalance')}: ${ovenBalance}`} + {isLoading ? : `${t('ovenBalance')}: ${ovenBalance}`} @@ -102,7 +116,11 @@ const OvenCardComponent: React.FC = ({ } /> - + {isLoading ? ( + + ) : ( + + )} {baker && ( @@ -114,13 +132,20 @@ const OvenCardComponent: React.FC = ({ )} - {t('outstandingCTez')}: {outStandingCtez} + {isLoading ? : `${t('outstandingCTez')} : ${outStandingCtez}`} {maxMintableCtez > 0 && ( - {t('currentUtilization')}: {((outStandingCtez / maxMintableCtez) * 100).toFixed(2)}% + {isLoading ? ( + + ) : ( + `${t('currentUtilization')} : ${( + (outStandingCtez / maxMintableCtez) * + 100 + ).toFixed(2)}%` + )} {isMonthAway && ( ⚠️ @@ -137,6 +162,7 @@ const OvenCardComponent: React.FC = ({ disableRipple disableFocusRipple endIcon={} + disabled={isLoading} > {t('actions')} diff --git a/frontend/app/src/contracts/cfmm.ts b/frontend/app/src/contracts/cfmm.ts index 7ae750e0..3d2e2abc 100644 --- a/frontend/app/src/contracts/cfmm.ts +++ b/frontend/app/src/contracts/cfmm.ts @@ -128,11 +128,11 @@ export const removeLiquidity = async ( return hash.opHash; }; -export const cashToToken = async (args: CashToTokenParams): Promise => { +export const tezToCash = async (args: CashToTokenParams): Promise => { const hash = await executeMethod( cfmm, - 'cashToToken', - [args.to, args.minTokensBought * 1e6, args.deadline.toISOString()], + 'tezToCash', + [args.to, args.minTokensBought * 1e6, args.deadline.toISOString(), 4], undefined, args.amount * 1e6, true, @@ -140,10 +140,7 @@ export const cashToToken = async (args: CashToTokenParams): Promise => { return hash; }; -export const tokenToCash = async ( - args: TokenToCashParams, - userAddress: string, -): Promise => { +export const cashToTez = async (args: TokenToCashParams, userAddress: string): Promise => { const tezos = getTezosInstance(); const CTezFa12 = await getCTezFa12Contract(); const batchOps: WalletParamsWithKind[] = await getTokenAllowanceOps( @@ -157,11 +154,12 @@ export const tokenToCash = async ( { kind: OpKind.TRANSACTION, ...cfmm.methods - .tokenToCash( + .cashToTez( args.to, args.tokensSold * 1e6, args.minCashBought * 1e6, args.deadline.toISOString(), + 4, ) .toTransferParams(), }, diff --git a/frontend/app/src/contracts/ctez.ts b/frontend/app/src/contracts/ctez.ts index 23dc52ed..6b1f2e6a 100644 --- a/frontend/app/src/contracts/ctez.ts +++ b/frontend/app/src/contracts/ctez.ts @@ -42,14 +42,18 @@ export const create = async ( op: Depositor, allowedDepositors?: string[], amount = 0, + confirmation = 0, + onConfirmation?: () => void | Promise, ): Promise => { const newOvenId = getLastOvenId(userAddress, cTez.address) + 1; const hash = await executeMethod( cTez, 'create', [newOvenId, bakerAddress, op, allowedDepositors], - undefined, + confirmation, amount, + false, + onConfirmation, ); saveLastOven(userAddress, cTez.address, newOvenId); return hash; diff --git a/frontend/app/src/contracts/utils.ts b/frontend/app/src/contracts/utils.ts index a8ec682f..740a505f 100644 --- a/frontend/app/src/contracts/utils.ts +++ b/frontend/app/src/contracts/utils.ts @@ -8,12 +8,21 @@ export const executeMethod = async ( confirmation = 0, amount = 0, mutez = false, + onConfirmation?: () => void | Promise, ): Promise => { const op = await contract.methods[methodName](...args).send({ amount: amount > 0 ? amount : undefined, mutez, }); - confirmation && (await op.confirmation(confirmation)); + if (confirmation > 0) { + op.confirmation(confirmation) + .then(() => { + onConfirmation && onConfirmation(); + }) + .catch(() => { + onConfirmation && onConfirmation(); + }); + } return op.opHash; }; diff --git a/frontend/app/src/interfaces/cfmm.ts b/frontend/app/src/interfaces/cfmm.ts index 149b5573..4c7311a4 100644 --- a/frontend/app/src/interfaces/cfmm.ts +++ b/frontend/app/src/interfaces/cfmm.ts @@ -40,10 +40,10 @@ export interface TokenToTokenParams { } export interface CfmmStorage { - tokenPool: BigNumber; + tezPool: BigNumber; cashPool: BigNumber; - pendingPoolUpdates: BigNumber; - tokenAddress: string; + target: BigNumber; + cashAddress: string; lqtAddress: string; lastOracleUpdate: Date; consumerEntrypoint: string; diff --git a/frontend/app/src/pages/BuySell/AddLiquidity.tsx b/frontend/app/src/pages/BuySell/AddLiquidity.tsx index 420a0572..62ff0669 100644 --- a/frontend/app/src/pages/BuySell/AddLiquidity.tsx +++ b/frontend/app/src/pages/BuySell/AddLiquidity.tsx @@ -48,10 +48,10 @@ const AddLiquidityComponent: React.FC = ({ t }) => { const calcMaxToken = (slippage: number, cashDeposited: number) => { if (cfmmStorage) { - const { tokenPool, cashPool } = cfmmStorage; + const { tezPool, cashPool } = cfmmStorage; const cash = cashDeposited * 1e6; const max = - Math.ceil(((cash * tokenPool.toNumber()) / cashPool.toNumber()) * (1 + slippage * 0.01)) / + Math.ceil(((cash * cashPool.toNumber()) / tezPool.toNumber()) * (1 + slippage * 0.01)) / 1e6; setMaxToken(Number(max.toFixed(6))); } else { @@ -61,10 +61,10 @@ const AddLiquidityComponent: React.FC = ({ t }) => { const calcMinPoolPercent = (slippage: number, cashDeposited: number) => { if (cfmmStorage) { - const { cashPool, lqtTotal } = cfmmStorage; + const { tezPool, lqtTotal } = cfmmStorage; const cash = cashDeposited * 1e6; const minLQTMinted = - ((cash * lqtTotal.toNumber()) / cashPool.toNumber()) * (1 - slippage * 0.01); + ((cash * lqtTotal.toNumber()) / tezPool.toNumber()) * (1 - slippage * 0.01); const minPool = (minLQTMinted / (lqtTotal.toNumber() + minLQTMinted)) * 100; setMinLQT(Number(Math.floor(minLQTMinted).toFixed())); setMinPoolPercent(Number(minPool.toFixed(6))); diff --git a/frontend/app/src/pages/BuySell/Conversion.tsx b/frontend/app/src/pages/BuySell/Conversion.tsx index 32609d72..9dfee721 100644 --- a/frontend/app/src/pages/BuySell/Conversion.tsx +++ b/frontend/app/src/pages/BuySell/Conversion.tsx @@ -21,12 +21,13 @@ import { useHistory } from 'react-router-dom'; import Page from '../../components/Page'; import FormikTextField from '../../components/TextField'; import { useWallet } from '../../wallet/hooks'; -import { cashToToken, cfmmError, tokenToCash } from '../../contracts/cfmm'; +import { tezToCash, cfmmError, cashToTez } from '../../contracts/cfmm'; import { TezosIcon } from '../../components/TezosIcon'; import { CTezIcon } from '../../components/CTezIcon/CTezIcon'; import { logger } from '../../utils/logger'; import { DEFAULT_SLIPPAGE } from '../../utils/globals'; import { useCfmmStorage } from '../../api/queries'; +import { tradeDTezForDCash, tradeDCashForDTez, FEE, FEE_DENOM } from '../../utils/cfmmUtils'; interface ConversionParams extends WithTranslation { formType: 'tezToCtez' | 'ctezToTez'; @@ -53,15 +54,28 @@ const ConvertComponent: React.FC = ({ t, formType }) => { const calcMinBuyValue = (slippage: number, amount: number) => { if (cfmmStorage) { - const { tokenPool, cashPool } = cfmmStorage; - const cashSold = amount * 1e6; - const [aPool, bPool] = - formType === 'tezToCtez' ? [tokenPool, cashPool] : [cashPool, tokenPool]; - const tokWithoutSlippage = - (cashSold * 997 * aPool.toNumber()) / (bPool.toNumber() * 1000 + cashSold * 997) / 1e6; - const tok = tokWithoutSlippage * (1 - slippage * 0.01); - setWithoutSlippage(Number(tokWithoutSlippage.toFixed(6))); - setMinBuyValue(Number(tok.toFixed(6))); + const { tezPool, cashPool, target } = cfmmStorage; + const bought = + formType === 'tezToCtez' + ? tradeDTezForDCash( + tezPool.toNumber(), + cashPool.toNumber(), + amount * 1e6, + target.toNumber(), + 4, + ) + : tradeDCashForDTez( + tezPool.toNumber(), + cashPool.toNumber(), + amount * 1e6, + target.toNumber(), + 4, + ); + + const boughtAfterFee = (bought * FEE) / FEE_DENOM; + const tok = boughtAfterFee * (1 - slippage * 0.01); + setWithoutSlippage(Number((Math.floor(boughtAfterFee) / 1e6).toFixed(6))); + setMinBuyValue(Number((Math.floor(tok) / 1e6).toFixed(6))); } else { setMinBuyValue(-1); } @@ -93,13 +107,13 @@ const ConvertComponent: React.FC = ({ t, formType }) => { const deadline = addMinutes(new Date(), formData.deadline); const result = formType === 'tezToCtez' - ? await cashToToken({ + ? await tezToCash({ amount: formData.amount, deadline, minTokensBought: minBuyValue, to: formData.to, }) - : await tokenToCash( + : await cashToTez( { deadline, minCashBought: minBuyValue, diff --git a/frontend/app/src/pages/BuySell/RemoveLiquidity.tsx b/frontend/app/src/pages/BuySell/RemoveLiquidity.tsx index 67c8a308..4e7af148 100644 --- a/frontend/app/src/pages/BuySell/RemoveLiquidity.tsx +++ b/frontend/app/src/pages/BuySell/RemoveLiquidity.tsx @@ -49,11 +49,11 @@ const RemoveLiquidityComponent: React.FC = ({ t }) => { const calcMinValues = (slippage: number, lqtBurned: number) => { if (cfmmStorage) { - const { cashPool, tokenPool, lqtTotal } = cfmmStorage; + const { cashPool, tezPool, lqtTotal } = cfmmStorage; const cashWithdraw = - ((lqtBurned * cashPool.toNumber()) / lqtTotal.toNumber()) * (1 - slippage * 0.01); + ((lqtBurned * tezPool.toNumber()) / lqtTotal.toNumber()) * (1 - slippage * 0.01); const tokenWithdraw = - ((lqtBurned * tokenPool.toNumber()) / lqtTotal.toNumber()) * (1 - slippage * 0.01); + ((lqtBurned * cashPool.toNumber()) / lqtTotal.toNumber()) * (1 - slippage * 0.01); setValues({ cashWithdraw: Number((cashWithdraw / 1e6).toFixed(6)), tokenWithdraw: Number((tokenWithdraw / 1e6).toFixed(6)), diff --git a/frontend/app/src/pages/CreateOven/CreateOven.tsx b/frontend/app/src/pages/CreateOven/CreateOven.tsx index 624a52a1..63e0da72 100644 --- a/frontend/app/src/pages/CreateOven/CreateOven.tsx +++ b/frontend/app/src/pages/CreateOven/CreateOven.tsx @@ -1,5 +1,7 @@ import React, { useEffect, useState } from 'react'; import { validateAddress } from '@taquito/utils'; +import { useQueryClient } from 'react-query'; +import { useDispatch } from 'react-redux'; import { withTranslation, WithTranslation } from 'react-i18next'; import * as Yup from 'yup'; import styled from '@emotion/styled'; @@ -21,6 +23,7 @@ import { import { FormikRadioGroup } from '../../components/FormikRadioGroup/FormikRadioGroup'; import { logger } from '../../utils/logger'; import { useDelegates } from '../../api/queries'; +import { StatsSlice } from '../../redux/slices/StatsSlice'; interface CreateVaultForm { delegate: string; @@ -35,10 +38,12 @@ const PaperStyled = styled(Paper)` const CreateOvenComponent: React.FC = ({ t }) => { const [{ pkh: userAddress }] = useWallet(); + const queryClient = useQueryClient(); const { data: delegates } = useDelegates(userAddress); const [delegate, setDelegate] = useState(''); const { addToast } = useToasts(); const history = useHistory(); + const dispatch = useDispatch(); const validationSchema = Yup.object().shape({ delegate: Yup.string() .test({ @@ -122,8 +127,15 @@ const CreateOvenComponent: React.FC = ({ t }) => { data.depositorOp, depositors, data.amount, + 1, + () => { + queryClient.invalidateQueries('ovenData'); + dispatch(StatsSlice.actions.setPendingTransaction(false)); + }, ); if (result) { + console.log(result); + dispatch(StatsSlice.actions.setPendingTransaction(true)); addToast(t('txSubmitted'), { appearance: 'success', autoDismiss: true, diff --git a/frontend/app/src/pages/MyOvenPage/index.tsx b/frontend/app/src/pages/MyOvenPage/index.tsx index 74e955c5..1da51863 100644 --- a/frontend/app/src/pages/MyOvenPage/index.tsx +++ b/frontend/app/src/pages/MyOvenPage/index.tsx @@ -1,4 +1,5 @@ import { CircularProgress, Grid, Box } from '@material-ui/core'; +import BigNumber from 'bignumber.js'; import { useSelector, useDispatch } from 'react-redux'; import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,8 +19,8 @@ import { toSerializeableOven, } from '../../utils/ovenUtils'; import { useCtezBaseStats, useOvenData } from '../../api/queries'; -import { isMonthFromLiquidation } from '../../api/contracts'; import { CTEZ_ADDRESS } from '../../utils/globals'; +import { isMonthFromLiquidation } from '../../utils/cfmmUtils'; export const MyOvenPage: React.FC = () => { const { t } = useTranslation(['common', 'header']); @@ -28,6 +29,7 @@ export const MyOvenPage: React.FC = () => { const originalTarget = useSelector((state: RootState) => Number(state.stats.baseStats?.originalTarget), ); + const pendingTx = useSelector((state: RootState) => state.stats.transactionPending); const { showActions } = useSelector((state: RootState) => state.oven); const [{ pkh: userAddress }] = useWallet(); const { data: ovenData, isLoading } = useOvenData(userAddress, extOvens); @@ -67,6 +69,20 @@ export const MyOvenPage: React.FC = () => { justifyItems="flex-start" spacing={3} > + {pendingTx && ( + + + + )} {ovenData && ovenData.length > 0 && ovenData @@ -128,7 +144,9 @@ export const MyOvenPage: React.FC = () => { )} {!isLoading && !userAddress && {t('signInToSeeOvens')}} - {!isLoading && userAddress && ovenData?.length === 0 && {t('noOvens')}} + {!isLoading && !pendingTx && userAddress && ovenData?.length === 0 && ( + {t('noOvens')} + )} {!isLoading && userAddress && ovenData && ovenData.length > 0 && ( ) => { state.baseStats = action.payload; }, + setPendingTransaction: (state, action: PayloadAction) => { + state.transactionPending = action.payload; + return state; + }, }, }); diff --git a/frontend/app/src/utils/cfmmUtils.ts b/frontend/app/src/utils/cfmmUtils.ts new file mode 100644 index 00000000..67188aff --- /dev/null +++ b/frontend/app/src/utils/cfmmUtils.ts @@ -0,0 +1,88 @@ +export const FEE = 9995; +export const FEE_DENOM = 10000; + +export const newtonDxToDyRecursive = ( + xp: number, + xp2: number, + x3yPlusY3x: number, + y: number, + dyApprox: number, + rounds: number, +): number => { + if (rounds <= 0) return dyApprox; + const yp = y - dyApprox; + const yp2 = Math.abs(yp * yp); + const num = Math.abs(xp * yp * (xp2 + yp2) - x3yPlusY3x); + const denom = xp * (xp2 + 3 * yp2); + const adjust = num / denom; + const newDyApprox = dyApprox + adjust; + return newtonDxToDyRecursive(xp, xp2, x3yPlusY3x, y, newDyApprox, rounds - 1); +}; + +export const newtonDxToDy = (x: number, y: number, dx: number, rounds: number): number => { + const xp = x + dx; + const xp2 = xp * xp; + const x3yPlusY3x = x * y * (x * x + y * y); + return newtonDxToDyRecursive(xp, xp2, x3yPlusY3x, y, 0, rounds); +}; + +export const tradeDTezForDCash = ( + tez: number, + cash: number, + dtez: number, + target: number, + rounds = 4, +): number => { + const x = tez * 2 ** 48; + const y = target * cash; + const dx = dtez * 2 ** 48; + const dyApprox = newtonDxToDy(x, y, dx, rounds); + const dCashApprox = dyApprox / target; + if (tez - dCashApprox <= 0) { + throw new Error('CASH POOL MINUS CASH WITHDRAWN IS NEGATIVE'); + } + return dCashApprox; +}; + +export const tradeDCashForDTez = ( + tez: number, + cash: number, + dcash: number, + target: number, + rounds = 4, +): number => { + const y = tez * 2 ** 48; + const x = target * cash; + const dx = target * dcash; + const dyApprox = newtonDxToDy(x, y, dx, rounds); + const dtezApprox = dyApprox / 2 ** 48; + if (tez - dtezApprox <= 0) { + throw new Error('TEZ POOL MINUS TEZ WITHDRAWN IS NEGATIVE'); + } + return dtezApprox; +}; + +export const calculateMarginalPrice = (tez: number, cash: number, target: number): number => { + const x = cash * target; + const y = tez * 2 ** 48; + const x2 = x * x; + const y2 = y * y; + const nom = tez * (3 * x2 + y2); + const denom = cash * (3 * y2 + x2); + return (nom * 2 ** 48) / denom / 2 ** 48; +}; + +export const isMonthFromLiquidation = ( + outstandingCtez: number, + target: number, + tezBalance: number, + currentDrift: number, +): boolean => { + return ( + outstandingCtez * + (target / 2 ** 48) * + (1 + currentDrift / 2 ** 48) ** ((365.25 * 24 * 3600) / 12) * + (16 / 15) > + tezBalance + ); +}; diff --git a/frontend/app/yarn.lock b/frontend/app/yarn.lock index 34431f81..346508f7 100644 --- a/frontend/app/yarn.lock +++ b/frontend/app/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@airgap/beacon-sdk@^2.2.5", "@airgap/beacon-sdk@^2.2.7": +"@airgap/beacon-sdk@^2.2.7": version "2.2.7" resolved "https://registry.yarnpkg.com/@airgap/beacon-sdk/-/beacon-sdk-2.2.7.tgz#8dcf041f9c98f2648d0d0a76698027717873ece3" integrity sha512-07tEUuRWjjX+eRWyX4r9b5TSwUvJZntiuUHLuaVFkZMa1E1BUuLKFB7z5KN9wBSzmUP6viASi3F2/5OFnomNkQ== @@ -15,6 +15,19 @@ libsodium-wrappers "0.7.8" qrcode-generator "1.4.4" +"@airgap/beacon-sdk@^2.3.0": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@airgap/beacon-sdk/-/beacon-sdk-2.3.2.tgz#310c76186a1cfc8afd2bab70f1b5a57da76dcb6b" + integrity sha512-PAZ3n/RJaaBqgwQrFo1xrpAcMY/x9W3eZKZyKZMZuKVTB60spiy3KvSj7OYsk+03/nvCB+qDpllkUGFZ5GxMJg== + dependencies: + "@types/chrome" "0.0.115" + "@types/libsodium-wrappers" "0.7.7" + axios "0.21.1" + bignumber.js "9.0.0" + bs58check "2.1.2" + libsodium-wrappers "0.7.8" + qrcode-generator "1.4.4" + "@babel/code-frame@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -2131,68 +2144,68 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" -"@taquito/beacon-wallet@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@taquito/beacon-wallet/-/beacon-wallet-9.0.1.tgz#ae97959bbdc17ffd6addf7636b09805718feeb9d" - integrity sha512-Nkkcfmotd1laTYP4uunI15OiCElBLv2fG8fDGY0Vb+0/2sokBPu60LEaQ1++2FBycRxWQ9Yph3ODxjNeIZMKMw== +"@taquito/beacon-wallet@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@taquito/beacon-wallet/-/beacon-wallet-10.1.1.tgz#72f73b6a33870f102989d500df9d8802501290bf" + integrity sha512-YtYmPKVO1GkA8/YbUq8h/utLlsDvNEA59XC+MUprh0QLyBCLtsh5RJmG5xxOFTRijmk1SMvDC18B1O9EBWALrw== dependencies: - "@airgap/beacon-sdk" "^2.2.5" - "@taquito/taquito" "^9.0.1" - "@taquito/utils" "^9.0.1" + "@airgap/beacon-sdk" "^2.3.0" + "@taquito/taquito" "^10.1.1" + "@taquito/utils" "^10.1.1" -"@taquito/http-utils@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@taquito/http-utils/-/http-utils-9.0.1.tgz#be8329520266d75e40665e2a38a4e6d8c01899a6" - integrity sha512-xSBToyKekno6Q0tPefLfYrhYelb6rigQavXQglCiZZt5N2u591sC8IEdYlq3iFe6teVg03phtVY8dGWGFkHOYw== +"@taquito/http-utils@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@taquito/http-utils/-/http-utils-10.1.1.tgz#207f1a57fc3bc5c55da3024dd460af198cba673d" + integrity sha512-nlaYjcfyAa89GT8YWYpbGrrR4hbQmUsrhojSX3emgyWTbN4QkZwke2h0eGJEOFQoUHeGSF1U8bVGOk6+jnlpIQ== dependencies: xhr2-cookies "^1.1.0" -"@taquito/michel-codec@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@taquito/michel-codec/-/michel-codec-9.0.1.tgz#bd066b6218dcc155ffc641877806465cf2f77a14" - integrity sha512-zUdE6P76p89dUZLjUz3aBaKy7qgGJv8Vkt7QaIcP0FmO78RIV796gaXVXfm9UByXeHnUG1ca3JYX2SKD/iYWpA== +"@taquito/michel-codec@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@taquito/michel-codec/-/michel-codec-10.1.1.tgz#eaba7d08fe2bc87778ed7ff9fa52c9a787b1b09e" + integrity sha512-/5gZetXAQ71b6Xrj1/gfgHopcR56xcVKCinEWIY8Bs2NK6RprNiXoh/cQVTybAyGwzesNCK8/7N7Fe6Os12yqA== -"@taquito/michelson-encoder@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@taquito/michelson-encoder/-/michelson-encoder-9.0.1.tgz#39fd2d52fa98b0421259af9bb90dc9ea2641786b" - integrity sha512-AoSEcKHpyKsIOcqubCxsA7gWf3+UO1wR67HMBZ9cQdNQKaRtdOm508dvL0DgWPQaNf6x1cw1s1eQn+p+N6nInQ== +"@taquito/michelson-encoder@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@taquito/michelson-encoder/-/michelson-encoder-10.1.1.tgz#0b27c4d4193627d09a8b7ead582cfddfdce69e7c" + integrity sha512-Q1qGfOu0Gbdfj9AbBVCc5sxBzEZkGxFSAPCK5nzCG3z1Jx3nGXDaYyonQa75lNWxYze9N+c3ETF4rd8OKsmihQ== dependencies: - "@taquito/rpc" "^9.0.1" - "@taquito/utils" "^9.0.1" + "@taquito/rpc" "^10.1.1" + "@taquito/utils" "^10.1.1" bignumber.js "^9.0.1" fast-json-stable-stringify "^2.1.0" -"@taquito/rpc@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@taquito/rpc/-/rpc-9.0.1.tgz#9ec943faf90565d0331552dea56d16c7bf20e8de" - integrity sha512-e78jsKxlxroC2wyfDMVIH+LCHQTMH6Liu0fTaZYiav3wIqZNK6LFgLSbLBoiT2iY0daoPFFHHDCAsbhoYU/WJQ== +"@taquito/rpc@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@taquito/rpc/-/rpc-10.1.1.tgz#889cce8551282fc1a05252cda8276bf3a709541d" + integrity sha512-AMYjBBWz3t0OXoVg8zge1OmcNUjpHFatSiV1RBr1g0fLNeFSlIqvsP4MwmR817ot95fxhOoIjgETPlDOOqwRDQ== dependencies: - "@taquito/http-utils" "^9.0.1" + "@taquito/http-utils" "^10.1.1" bignumber.js "^9.0.1" - lodash "^4.17.20" + lodash "^4.17.21" -"@taquito/taquito@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@taquito/taquito/-/taquito-9.0.1.tgz#1195411e9cc8c88f7c9c59ec633ba2f1aab93919" - integrity sha512-+7UkZndDKVoPla+0WFPTxK5EfTjwbWyzthinzOA3OrfPLJIeYUMd5LJcR02r9lL4YXqLwIBgrrPoSu/CqiUxPQ== - dependencies: - "@taquito/http-utils" "^9.0.1" - "@taquito/michel-codec" "^9.0.1" - "@taquito/michelson-encoder" "^9.0.1" - "@taquito/rpc" "^9.0.1" - "@taquito/utils" "^9.0.1" +"@taquito/taquito@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@taquito/taquito/-/taquito-10.1.1.tgz#b5e79544b66d32199f4545acd3132b640915e2bf" + integrity sha512-95MJgfErpKCRDcUpt9C5xDeCA7cwugWIETSPCP8h22NyN5bovRIb92gvk1uun0a/pm9sBPfTSiG/2Q7rXsc7yQ== + dependencies: + "@taquito/http-utils" "^10.1.1" + "@taquito/michel-codec" "^10.1.1" + "@taquito/michelson-encoder" "^10.1.1" + "@taquito/rpc" "^10.1.1" + "@taquito/utils" "^10.1.1" bignumber.js "^9.0.1" - rx-sandbox "^1.0.3" + rx-sandbox "^1.0.4" rxjs "^6.6.3" -"@taquito/utils@^9.0.1": - version "9.0.1" - resolved "https://registry.yarnpkg.com/@taquito/utils/-/utils-9.0.1.tgz#add8b35071895ed316abe20b1d308df46062cf9f" - integrity sha512-b9vLbcjNtOm3popJPbFjWdU69ydz15QwHuRyiNPI5eEd47624+2hv1wMg1yyOdxyJhfKLhkfO/46B+lETjLopg== +"@taquito/utils@^10.1.1": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@taquito/utils/-/utils-10.1.1.tgz#d17eddd9e65e42a1f01787417ea0a86a8797d3f3" + integrity sha512-fqY5uPAc5Yuns0YwgzSToKuvSN0ba/ZOWpaNgCSaFeOyWMW2ZAIDpfDcr70+MGqe81EwKM1FO6r61hlb06ElFw== dependencies: blakejs "^1.1.0" bs58check "^2.1.2" - buffer "^5.6.0" + buffer "^6.0.3" "@testing-library/dom@^7.28.1": version "7.29.6" @@ -3913,13 +3926,13 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -buffer@^5.6.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" - ieee754 "^1.1.13" + ieee754 "^1.2.1" builtin-modules@^3.1.0: version "3.2.0" @@ -4066,9 +4079,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181: - version "1.0.30001191" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz#bacb432b6701f690c8c5f7c680166b9a9f0843d9" - integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw== + version "1.0.30001257" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz" + integrity sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA== capture-exit@^2.0.0: version "2.0.0" @@ -6927,7 +6940,7 @@ identity-obj-proxy@3.0.0: dependencies: harmony-reflect "^1.4.6" -ieee754@^1.1.13, ieee754@^1.1.4: +ieee754@^1.1.4, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -11230,7 +11243,7 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rx-sandbox@^1.0.3: +rx-sandbox@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/rx-sandbox/-/rx-sandbox-1.0.4.tgz#821a1d64e5f0d88658da7a5dbbd735b13277648b" integrity sha512-+/9MHDYNoF9ca/2RR+L2LloXXeQyIR3k/wjK03IicrxxlbkhmKF4ejPiWeafMWDg7otF+pnX5NE/8v/rX6ICJA== diff --git a/half_dex.mligo b/half_dex.mligo new file mode 100644 index 00000000..bceb76f7 --- /dev/null +++ b/half_dex.mligo @@ -0,0 +1,348 @@ +#include "stdctez.mligo" +#import "context.mligo" Context + +(** A half dex is defined by: + - An ordered liquidity share [(tau_0, tau_1)] + - A reserve of the 'self' token [r0 : tau_0] + - A reserve of the 'proceeds' token [r1 : tau_1] + - A subsidy owed by ovens to the dex (in ctez) [s : CTEZ] + - A fee index [f : nat] + Each account may own some shares in the dex. + + The dex is parameterised by a capability context for: + - transferring the self token + - transferring the proceeds token + - computing the target quantity of the 'self' token in the half dex +*) + +type environment = + { transfer_self : Context.t -> address -> address -> nat -> operation + ; transfer_proceeds : Context.t -> address -> nat -> operation + ; target_self_reserves : Context.t -> nat + } + +module Curve = struct + (** The marginal price [dp/du] is the derivative of the price function [p(u)] with respect to the + characteristic quantity [u = min(q/Q, 1)] where [q] is the current amount of the dex's 'self' + token and [Q] is the target amount. + + The marginal price function is given as [dp/du(u) = target * (21 - 3 * u + 3 * u^2 - u^3) / 20]. + Meaning the price of dex's 'self' token in 'proceed' token units is given by + {v + p(u_0 -> u_1) = ∫_{u_0}^{u_1} dp/du(u) du + = [target * (21 * u - 3 * u^2 / 2 + u^3 - u^4 / 4) / 20]_{u_1}^{u_0} + v} + for [u_1 < u_0] + + For a swap we need to determine [y] for a given [x] such that [p(q/q -> (q - y) / Q) = x] where + [x] is the amount of 'proceed' token units to be traded for [y] 'self' token units. + + Solving the above equation for [y] gives irrational solutions. We instead use Newton's method to + find an approximate solution. The Newton step is given as + {v + y_{i + 1} = y_i - p(q/Q, (q - y_i)/Q) / p'(q/Q, (q - y_i)/Q) + = FullySimplify[%] + = 3 y_i⁴ + 6 y_i² (q - Q)² + 8 y_i³ (-q + Q) + 80 Q³ x + ----------------------------------------------------------- + 4 ((y_i - q)³ + 3 (y_i - q)² Q + 3 (y_i - q) Q² + 21 Q³) + v} + + Note that since marginal price function is generally very nearly linear in the region [0, 1], + so Newton converges stupidly fast. + *) + + let newton_step (x : nat) (y : nat) (q : nat) (_Q : nat) : int = + (* Computes + 3 y⁴ + 6 y² (q - Q)² + 8 y³ (-q + Q) + 80 Q³ x + --------------------------------------------------- + 4 ((y - q)³ + 3 (y - q)² Q + 3 (y - q) Q² + 21 Q³) + *) + let dq = min y q in + let q_m_Q = q - _Q in + let dq_m_q = dq - q in + let dq_m_q_sq = dq_m_q * dq_m_q in + let dq_m_q_cu = dq_m_q_sq * dq_m_q in + let _Q_sq = _Q * _Q in + let _Q_cu = _Q_sq * _Q in + let num = 3 * dq * dq * dq * dq + 6 * dq * dq * q_m_Q * q_m_Q + 8 * dq * dq * dq * (-q_m_Q) + 80 * _Q_cu * x in + let denom = 4 * (dq_m_q_cu + 3 * dq_m_q_sq * _Q + 3 * dq_m_q * _Q_sq + 21 * _Q_cu) in + num / denom + + (** [swap_amount x q _Q] returns the swap amount for trading [x] units of the proceeds token + for a half dex with total quantity [q] and target quantity [_Q]. + *) + let swap_amount (x : nat) (q : nat) (_Q : nat) : nat = + let q = min q _Q in + (* Initial guess for [y] is [x] *) + let y = x in + let y = clamp_nat (newton_step x y q _Q) in + let y = clamp_nat (newton_step x y q _Q) in + let y = clamp_nat (newton_step x y q _Q) in + let result = y - y / 1_000_000_000 - 1 in + match is_nat result with + | None -> failwith "trade size too small" + | Some x -> x +end + +type liquidity_owner = + { liquidity_shares : nat (** the amount of liquidity shares owned by the account. *) + ; proceeds_owed : nat (** the amount of the proceeds token owed to the dex by the account. *) + ; subsidy_owed : nat (** the amount of ctez subsidy owed to the dex by the account. *) + } + +let default_liqudity_owner = + { liquidity_shares = 0n ; proceeds_owed = 0n ; subsidy_owed = 0n } + +type t = + { liquidity_owners : (address, liquidity_owner) big_map (** map of liquidity owners. *) + ; total_liquidity_shares : nat (** total amount of liquidity shares. *) + ; self_reserves : nat (** total amount of liquidity. *) + ; proceeds_reserves : nat (** total amount accumulated from proceeds. *) + ; subsidy_reserves : nat (** total amount accumulated from subsidy. *) + ; fee_index : Float48.t (** the fee index. *) + } + +let find_liquidity_owner (t : t) (owner : address) : liquidity_owner = + Option.value default_liqudity_owner (Big_map.find_opt owner t.liquidity_owners) + +let set_liquidity_owner (t : t) (owner : address) (liquidity_owner : liquidity_owner) : t = + { t with liquidity_owners = Big_map.update owner (Some liquidity_owner) t.liquidity_owners } + +let update_liquidity_owner (t : t) (owner : address) (f : liquidity_owner -> liquidity_owner) : t = + let liquidity_owner = find_liquidity_owner t owner in + let liquidity_owner = f liquidity_owner in + set_liquidity_owner t owner liquidity_owner + +type deposit = + { owner: address (** the address that will own the liquidity shares. *) + ; amount_deposited: nat (** the amount of the 'self' token to deposit. *) + ; min_liquidity: nat (** the minimum amount of liquidity shares to add. *) + ; deadline : timestamp (** the deadline for the transaction. *) + } + +[@inline] +let redeem_amount (x : nat) (reserve : nat) (total : nat) : nat = + (* The redeem rate is defined as + RX_i(t_0, t_1) := r_i / total(t_0, t_1) + *) + ceildiv (x * reserve) total + +[@inline] +let redeem_amount_inverted (lqt : nat) (reserve: nat) (total: nat) : nat = + // The redeem amount is defined as + // lqt = x RX_i(t_0, t_1) + // Thus + // x = (lqt * total(t_0, t_1)) / reserve + ceildiv (lqt * total) reserve + +let deposit + (t : t) + (ctxt : Context.t) + (env : environment) + ({ owner; amount_deposited; min_liquidity; deadline } : deposit) + () + : t with_operations + = + let d_liquidity = + redeem_amount_inverted amount_deposited t.self_reserves t.total_liquidity_shares + in + let () = + assert_with_error + (d_liquidity >= min_liquidity) + "transaction would create insufficient liquidity" + in + let () = assert_with_error (Tezos.get_now () <= deadline) "deadline has passed" in + let d_proceeds = + redeem_amount d_liquidity t.proceeds_reserves t.total_liquidity_shares + in + let d_subsidy = + redeem_amount d_liquidity t.subsidy_reserves t.total_liquidity_shares + in + let t = + update_liquidity_owner t owner (fun liquidity_owner -> + { liquidity_owner with + liquidity_shares = liquidity_owner.liquidity_shares + d_liquidity + ; proceeds_owed = liquidity_owner.proceeds_owed + d_proceeds + ; subsidy_owed = liquidity_owner.subsidy_owed + d_subsidy + }) + in + let t = + { t with + total_liquidity_shares = t.total_liquidity_shares + d_liquidity + ; self_reserves = t.self_reserves + amount_deposited + } + in + let receive_self = + env.transfer_self ctxt owner (Tezos.get_self_address ()) amount_deposited + in + [ receive_self ], t + + +type redeem = + { to_: address (** the address to receive the tokens *) + ; liquidity_redeemed : nat (** the amount of liquidity shares to redeem *) + ; min_self_received : nat (* minimum amount of tez to receive *) + ; min_proceeds_received : nat (* minimum amount of ctez to receive *) + ; min_subsidy_received : nat (* minimum amount of ctez subsidy to receive *) + ; deadline : timestamp (* deadline for the transaction *) + } + +[@inline] +let subtract_debt (debt : nat) (amt : nat) = + if amt < debt then (abs (debt - amt), 0n) + else (0n, abs (amt - debt)) + +let redeem + (t : t) + (ctxt : Context.t) + (env : environment) + ({ to_ + ; liquidity_redeemed + ; min_self_received + ; min_proceeds_received + ; min_subsidy_received + ; deadline + } : + redeem) + : t with_operations + = + let () = assert_with_error (Tezos.get_now () <= deadline) "deadline has passed" in + let owner = Tezos.get_sender () in + let liquidity_owner = find_liquidity_owner t owner in + let () = + assert_with_error + (liquidity_owner.liquidity_shares >= liquidity_redeemed) + "insufficient liquidity" + in + let self_redeemed = + redeem_amount liquidity_redeemed t.self_reserves t.total_liquidity_shares + in + let () = + assert_with_error + (self_redeemed >= min_self_received) + "insufficient self would be received" + in + let proceeds_owed, proceeds_redeemed = + subtract_debt + liquidity_owner.proceeds_owed + (redeem_amount liquidity_redeemed t.proceeds_reserves t.total_liquidity_shares) + in + let () = + assert_with_error + (proceeds_redeemed >= min_proceeds_received) + "insufficient proceeds would be received" + in + let subsidy_owed, subsidy_redeemed = + subtract_debt + liquidity_owner.subsidy_owed + (redeem_amount liquidity_redeemed t.subsidy_reserves t.total_liquidity_shares) + in + let () = + assert_with_error + (subsidy_redeemed >= min_subsidy_received) + "insufficient subsidy would be received" + in + let t = + update_liquidity_owner t owner (fun liquidity_owner -> + { liquidity_owner with + liquidity_shares = abs (liquidity_owner.liquidity_shares - liquidity_redeemed) + ; proceeds_owed + ; subsidy_owed + }) + in + let t = + { t with + total_liquidity_shares = abs (t.total_liquidity_shares - liquidity_redeemed) + ; self_reserves = abs (t.self_reserves - self_redeemed) + ; proceeds_reserves = abs (t.proceeds_reserves - proceeds_redeemed) + ; subsidy_reserves = abs (t.subsidy_reserves - subsidy_redeemed) + } + in + let receive_self = env.transfer_self ctxt (Tezos.get_self_address ()) to_ self_redeemed in + let receive_proceeds = + env.transfer_proceeds ctxt to_ proceeds_redeemed + in + let receive_subsidy = + Context.transfer_ctez ctxt (Tezos.get_self_address ()) to_ subsidy_redeemed + in + [ receive_self; receive_proceeds; receive_subsidy ], t + +type swap = + { to_: address (** address that will own the 'self' tokens in the swap *) + ; deadline : timestamp (** deadline for the transaction *) + ; proceeds_amount : nat (** the amount of the 'proceed' token used to buy the 'self' token *) + ; min_self : nat (** the minimum amount of 'self' tokens to receive *) + } + +let swap + (t : t) + (ctxt : Context.t) + (env : environment) + ({ to_; proceeds_amount; min_self; deadline } : swap) + : t with_operations + = + let () = assert_with_error (Tezos.get_now () <= deadline) "deadline has passed" in + let self_to_sell = Curve.swap_amount proceeds_amount t.self_reserves (env.target_self_reserves ctxt) in + let () = + assert_with_error (self_to_sell >= min_self) "insufficient 'self' would be bought" + in + let () = + assert_with_error (self_to_sell <= t.self_reserves) "insufficient 'self' in the dex" + in + let t = + { t with + self_reserves = abs (t.self_reserves - self_to_sell) + ; proceeds_reserves = t.proceeds_reserves + proceeds_amount + } + in + let receive_self = env.transfer_self ctxt (Tezos.get_self_address ()) to_ self_to_sell in + [ receive_self ], t + +type collect_proceeds_and_subsidy = + { to_: address } + +let collect_proceeds_and_subsidy + (t : t) + (ctxt : Context.t) + (env : environment) + ({ to_ } : collect_proceeds_and_subsidy) + : t with_operations + = + let owner = Tezos.get_sender () in + let liquidity_owner = find_liquidity_owner t owner in + (* compute withdrawable amount of proceeds *) + let share_of_proceeds = + redeem_amount + liquidity_owner.liquidity_shares + t.proceeds_reserves + t.total_liquidity_shares + in + let amount_proceeds_withdrawn = + clamp_nat (share_of_proceeds - liquidity_owner.proceeds_owed) + in + (* compute withdrawable amount of subsidy *) + let share_of_subsidy = + redeem_amount + liquidity_owner.liquidity_shares + t.subsidy_reserves + t.total_liquidity_shares + in + let amount_subsidy_withdrawn = + clamp_nat (share_of_subsidy - liquidity_owner.subsidy_owed) + in + let t = + set_liquidity_owner + t + owner + { liquidity_owner with + proceeds_owed = share_of_proceeds + ; subsidy_owed = share_of_subsidy + } + in + let receive_proceeds = + env.transfer_proceeds ctxt to_ amount_proceeds_withdrawn + in + let receive_subsidy = + Context.transfer_ctez ctxt (Tezos.get_self_address ()) to_ amount_subsidy_withdrawn + in + [ receive_proceeds; receive_subsidy ], t \ No newline at end of file diff --git a/limit_dex.mligo b/limit_dex.mligo new file mode 100644 index 00000000..7d91f88b --- /dev/null +++ b/limit_dex.mligo @@ -0,0 +1,503 @@ +// TODO: +// - Model of ctez +// - Swap function +// - Handle subsidy + + + +[@inline] +let ceildiv (numerator : nat) (denominator : nat) : nat = abs ((- numerator) / (int denominator)) + + +// module Half_dex = struct +// (** A half dex is defined by: +// - An ordered liquidity share [(tau_0, tau_1)] +// - A reserve of the 'self' token [r0 : tau_0] +// - A reserve of the 'proceeds' token [r1 : tau_1] +// Each account may own some shares in the dex. +// *) + +// let transfer_self (from_ : address) (to_ : address) (value : nat) : operation = +// failwith "TODO" + +// type liquidity_owner = +// { liquidity_shares : nat (** the amount of liquidity shares owned by the account. *) +// ; proceeds_owed : nat (** the amount of the proceeds token owed to the dex by the account. *) +// ; subsidy_owed : nat (** the amount of ctez subsidy owed to the dex by the account. *) +// } + +// let default_liqudity_owner = { liquidity_shares = 0n ; proceeds_owed = 0n ; subsidy_owed = 0n } + +// type t = +// { liquidity_owners : (address, liquidity_owner) big_map (** map of liquidity owners. *) +// ; total_liquidity_shares : nat (** total amount of liquidity shares. *) +// ; self_reserves : nat (** total amount of liquidity. *) +// ; proceeds_reserves : nat (** total amount accumulated from proceeds. *) +// } + + +// type deposit = +// { owner: address (** the address that will own the liquidity shares. *) +// ; amount_deposited: nat (** the amount of the 'self' token to deposit. *) +// ; min_liquidity: nat (** the minimum amount of liquidity shares to add. *) +// ; deadline : timestamp (** the deadline for the transaction. *) +// } + + +// [@inline] +// let redeem_amount (x : nat) (reserve : nat) (total : nat) : nat = +// (* The redeem rate is defined as +// RX_i(t_0, t_1) := r_i / total(t_0, t_1) +// *) +// ceildiv (x * reserve) total + +// [@inline] +// let redeem_amount_inverted (lqt : nat) (reserve: nat) (total: nat) : nat = +// // The redeem amount is defined as +// // lqt = x RX_i(t_0, t_1) +// // Thus +// // x = (lqt * total(t_0, t_1)) / reserve +// ceildiv (lqt * total) reserve + +// let find_liquidity_owner (t : t) (owner : address) : liquidity_owner = +// Option.value default_liqudity_owner (Big_map.find_opt owner t.liquidity_owners) + +// let set_liquidity_owner (t : t) (owner : address) (liquidity_owner : liquidity_owner) : t = +// { t with liquidity_owners = Big_map.update owner (Some liquidity_owner) t.liquidity_owners } + +// let update_liquidity_owner (t : t) (owner : address) (f : liquidity_owner -> liquidity_owner) : t = +// let liquidity_owner = find_liquidity_owner t owner in +// let liquidity_owner = f liquidity_owner in +// set_liquidity_owner t owner liquidity_owner + +// let deposit (t : t) ({ owner; amount_deposited; min_liquidity; deadline } : deposit) () +// : t * operation list +// = +// let d_liquidity = +// redeem_amount_inverted amount_deposited t.self_reserves t.total_liquidity_shares +// in +// let () = +// assert_with_error +// (d_liquidity >= min_liquidity) +// "transaction would create insufficient liquidity" +// in +// let () = +// assert_with_error +// (Tezos.get_now () <= deadline) +// "deadline has passed" +// in +// let d_proceeds = +// redeem_amount d_liquidity t.proceeds_reserves t.total_liquidity_shares +// in +// let t = +// update_liquidity_owner t owner (fun liquidity_owner -> +// { liquidity_owner with +// liquidity_shares = liquidity_owner.liquidity_shares + d_liquidity +// ; proceeds_owed = liquidity_owner.proceeds_owed + d_proceeds +// }) +// in +// let t = +// { t with +// total_liquidity_shares = t.total_liquidity_shares + d_liquidity +// ; self_reserves = t.self_reserves + amount_deposited +// } +// in +// let receive_self = transfer_self owner (Tezos.get_self_address ()) amount_deposited in +// t, [ receive_self ] + + +// type redeem = +// { to_: address (** the address to receive the tokens *) +// ; liquidity_redeemed : nat (** the amount of liquidity shares to redeem *) +// ; min_self_received : nat (* minimum amount of tez to receive *) +// ; min_proceeds_received : nat (* minimum amount of ctez to receive *) +// ; min_subsidy_received : nat (* minimum amount of ctez subsidy to receive *) +// ; deadline : timestamp (* deadline for the transaction *) +// } + +// let redeem (t : t) ({ to_; liquidity_redeemed; min_self_received; min_proceeds_received; min_subsidy_received; deadline } : redeem) : t * operation list = +// let () = assert_with_error (Tezos.get_now () <= deadline) "deadline has passed" in +// let owner = Tezos.get_sender () in +// let liquidity_owner = find_liquidity_owner t owner in +// let () = assert_with_error (liquidity_owner.liquidity_shares >= liquidity_redeemed) "insufficient liquidity" in +// let self_redeemed = redeem_amount liquidity_redeemed t.self_reserves t.total_liquidity_shares in +// let owed_proceeds, proceeds_redeemed = +// (* Proceeds must account for netting the owed amount *) +// let v = redeem_amount liquidity_redeemed t.proceeds_reserves t.total_liquidity_shares in +// if v < liquidity_owner.proceeds_owed then +// (abs (liquidity_owner.proceeds_owed - v), 0n) +// else +// (0n, abs (v - liquidity_owner.proceeds_owed)) +// in +// let () = assert_with_error (proceeds_redeemed >= min_proceeds_received) "insufficient proceeds would be received" in +// let t = update_liquidity_owner t owner (fun liquidity_owner -> +// { liquidity_owner with +// liquidity_shares = abs (liquidity_owner.liquidity_shares - liquidity_redeemed) +// ; proceeds_owed = liquidity_owner.proceeds_owed + owed_proceeds +// }) +// in +// let t = { t with +// total_liquidity_shares = abs (t.total_liquidity_shares - liquidity_redeemed) +// ; self_reserves = abs (t.self_reserves - self_redeemed) +// ; proceeds_reserves = abs (t.proceeds_reserves - proceeds_redeemed) +// } +// in +// let receive_ctez = Tezos.transaction (param.to_, (s.ctez_token_contract, ctez_removed)) 0mutez s.liquidity_dex_address in +// let receive_subsidy = Tezos.transaction (param.to_, (s.ctez_token_contract, subsidy_to_receive)) 0mutez s.liquidity_dex_address in +// let receive_tez = Tezos.transaction () (tez_to_receive * 1mutez) param.to_ in +// (t, [receive_ctez; receive_subsidy; receive_tez]) + + +// type swap = +// { to_: address (** address that will own the 'self' tokens in the swap *) +// ; deadline : timestamp (** deadline for the transaction *) +// ; proceeds_amount : nat (** minimum amount of tez to buy *) +// ; min_self : nat (** the *) +// } + +// let swap (t : t) +// end + + + +type add_tez_liquidity = +[@layout:comb] +{ + owner : address ; (* address that will own the liqudity *) + minLiquidity : nat ; (* minimum amount of liquidity to add *) + deadline : timestamp ; (* deadline for the transaction *) +} + +type add_ctez_liquidity = +[@layout:comb] +{ + owner : address ; (* address that will own the liqudity *) + minLiquidity : nat ; (* minimum amount of liquidity to add *) + deadline : timestamp ; (* deadline for the transaction *) + ctezDeposited : nat ; (* amount of ctez to deposit *) +} + +type remove_tez_liquidity = +[@layout:comb] +{ + [@annot:to] to_: address ; (* address to receive to *) + lpt : nat ; (* amount of liquidity to remove *) + minTezReceived : nat ; (* minimum amount of tez to receive *) + minCtezReceived : nat ; (* minimum amount of ctez to receive *) + minSubsidyReceived : nat ; (* minimum amount of ctez subsidy to receive *) + deadline : timestamp ; (* deadline for the transaction *) +} + +type remove_ctez_liquidity = +[@layout:comb] +{ + [@annot:to] to_: address ; (* address to receive to *) + lpt : nat ; (* amount of liquidity to remove *) + minTezReceived : nat ; (* minimum amount of tez to receive *) + minCtezReceived : nat ; (* minimum amount of ctez to receive *) + minSubsidyReceived : nat ; (* minimum amount of ctez subsidy to receive *) + deadline : timestamp ; (* deadline for the transaction *) +} + +type tez_to_ctez = +[@layout:comb] +{ + [@annot:to] to_: address ; (* address that will own the ctez *) + deadline : timestamp ; (* deadline for the transaction *) + minCtezBought : nat ; (* minimum amount of ctez to buy *) +} + +type ctez_to_tez = +[@layout:comb] +{ + [@annot:to] to_: address ; (* address that will own the tez *) + deadline : timestamp ; (* deadline for the transaction *) + minTezBought : nat ; (* minimum amount of tez to buy *) + ctezSold : nat ; (* amount of ctez to sell *) +} + +type withdraw_for_tez_liquidity = +[@layout:comb] +{ + [@annot:to] to_: address ; (* address to withdraw to *) +} + +type withdraw_for_ctez_half_dex = +[@layout:comb] +{ + [@annot:to] to_: address ; (* address to withdraw to, note that here you receive both ctez and tez + because ctez is received as part of the subsidy *) +} + +type liquidity_owner = +[@layout:comb] +{ + lpt : nat ; (* LP token amount *) + owed : nat ; (* amount of the proceeds token owed to the contract *) + subsidy_owed : nat ; (* amount of ctez subsidy owed to the contract *) +} + + +type half_dex = +[@layout:comb] +{ + liquidity_owners : (address, liquidity_owner) big_map ; (* map of liquidity owners *) + total_lpt : nat ; (* total amount of liquidity tokens *) + total_liquidity : nat ; (* total amount of liquidity *) + total_proceeds : nat ; (* total amount accumulated from proceeds *) + total_subsidy : nat ; (* total amount accumulated from subsidy *) +} + + +type fa12_transfer = + [@layout:comb] + { [@annot:from] address_from : address; + [@annot:to] address_to : address; + value : nat } + +type storage = +[@layout:comb] +{ + sell_ctez : half_dex ; + sell_tez : half_dex ; + target : nat ; (* target / 2^48 is the target price of ctez in tez *) (* todo, logic for update *) + _Q : nat ; (* Q is the desired quantity of ctez in the ctez half dex, + floor(Q * target) is the desired quantity of tez in the tez half dex *) + liquidity_dex_address : address ; (* address of the liquidity dex *) + ctez_token_contract : address ; (* address of the ctez token contract *) + ctez_contract : address ; (* address of the ctez contract *) + last_update : nat; +} + +// retrieve _Q and target + + + +let update_ctez_contract_if_needed (s : storage) : operation list * storage = + let curr_level = Tezos.get_level () in + if s.last_update <> curr_level then + let ctez_contract = (Tezos.get_entrypoint "%dex_update" s.ctez_contract : (nat * nat) contract) in + let operation = Tezos.transaction (s.sell_ctez.total_liquidity, s.sell_tez.total_liquidity) 0mutez ctez_contract in + let target , _Q = Option.value_with_error "dex_info entrypoint must exist" (Tezos.call_view "%dex_info" () s.ctez_contract) in + ([operation], {s with last_update = curr_level; target = target; _Q = _Q}) + else + ([], s) + + +[@inline] +let ceildiv (numerator : nat) (denominator : nat) : nat = abs ((- numerator) / (int denominator)) + + +[@inline] +let redeem_amount (x : nat) (reserve : nat) (total : nat) : nat = + // The redeem rate is defined as + // RX_i(t_0, t_1) := r_i / total(t_0, t_1) + // The redeem amount is defined as + // v = x / RX_i(t_0, t_1) = (x * total(t_0, t_1)) / reserve + (x * total) / reserve + + +[@entry] +let add_ctez_liquidity (param : add_ctez_liquidity) (s : storage) : storage * operation list = + let d_lpt = redeem_amount param.ctezDeposited s.sell_ctez.total_liquidity s.sell_ctez.total_lpt in + let () = assert_with_error (d_lpt >= param.minLiquidity) "transaction would create insufficient liquidity" in + let () = assert_with_error (Tezos.get_now () <= param.deadline) "deadline has passed" in + // lpt is going to be lpt + d_lpt + // ctez is going to be ctez + d_ctez + // if the owner already has liquidity, we need to update the owed amount + // otherwise we need to create a new liquidity owner + let liquidity_owner = + Option.value + { lpt = 0n ; owed = 0n ; subsidy_owed = 0n} + (Big_map.find_opt param.owner s.sell_ctez.liquidity_owners) + in + let d_tez = ceildiv (s.sell_ctez.total_proceeds * d_lpt) s.sell_ctez.total_lpt in + let d_subsidy_owed = ceildiv (s.sell_ctez.total_subsidy * d_lpt) s.sell_ctez.total_lpt in + // Update liquidity owner + let liquidity_owner = { liquidity_owner with + lpt = liquidity_owner.lpt + d_lpt ; + owed = liquidity_owner.owed + d_tez ; + subsidy_owed = liquidity_owner.subsidy_owed + d_subsidy_owed } in + let liquidity_owners = Big_map.update param.owner (Some liquidity_owner) s.sell_ctez.liquidity_owners in + + let sell_ctez = {s.sell_ctez with + liquidity_owners = liquidity_owners ; + total_lpt = s.sell_ctez.total_lpt + d_lpt ; + total_liquidity = s.sell_ctez.total_liquidity + param.ctezDeposited ; + } in + + let receive_ctez = Tezos.transaction (param.owner, (s.liquidity_dex_address, param.ctezDeposited)) 0mutez s.ctez_token_contract in + ({s with sell_ctez = half_dex}, [receive_ctez]) + +[@entry] +let remove_ctez_liquidity (param : remove_ctez_liquidity) (s : storage) : storage * operation list = + let () = assert_with_error (Tezos.get_now () <= param.deadline) "deadline has passed" in + let ctez_removed = (param.lpt * s.sell_ctez.total_liquidity) / s.sell_ctez.total_lpt in + let tez_removed = (param.lpt * s.sell_ctez.total_proceeds) / s.sell_ctez.total_lpt in + let subsidy_removed = (param.lpt * s.sell_ctez.total_subsidy) / s.sell_ctez.total_lpt in + let owner = Tezos.get_sender () in + let liquidity_owner = Option.unopt_with_error (Big_map.find_opt owner s.sell_ctez.liquidity_owners) "no liquidity owner" in + let () = assert_with_error (liquidity_owner.lpt >= param.lpt) "insufficient liquidity" in + let () = assert_with_error (ctez_removed >= param.minCtezReceived) "insufficient ctez would be received" in + + (* compute the amount of tez to receive after netting the owed amount *) + let tez_to_receive = tez_removed - liquidity_owner.owed in + let () = assert_with_error (tez_to_receive >= int param.minTezReceived) "insufficient tez would be received" in + let (owed, tez_to_receive) = + if tez_to_receive < 0 then + (abs (liquidity_owner.owed - tez_removed), 0n) + else + (0n, abs tez_to_receive) + in + (* computed the amount of subsidy to recieve after netting the owed subsidy amount *) + + + let subsidy_to_receive = subsidy_removed - liquidity_owner.subsidy_owed in + let () = assert_with_error (subsidy_to_receive >= int param.minSubsidyReceived) "insufficient subsidy would be received" in + let (subsidy_owed, subsidy_to_receive) = + if subsidy_to_receive < 0 then + (abs (liquidity_owner.subsidy_owed - subsidy_removed), 0n) + else + (0n, abs subsidy_to_receive) + in + + let liquidity_ower = { liquidity_owner with + lpt = abs (liquidity_owner.lpt - param.lpt) ; + owed = owed ; + subsidy_owed = subsidy_owed } in + let liquidity_owners = Big_map.update owner (Some liquidity_owner) s.sell_ctez.liquidity_owners in + + let sell_ctez = {s.sell_ctez with + liquidity_owners = liquidity_owners ; + total_lpt = abs (s.sell_ctez.total_lpt - param.lpt) ; + total_liquidity = abs (s.sell_ctez.total_liquidity - ctez_removed) ; + total_proceeds = abs (s.sell_ctez.total_proceeds - tez_removed) ; + total_subsidy = abs (s.sell_ctez.total_subsidy - subsidy_removed) ; + } in + let receive_ctez = Tezos.transaction (param.to_, (s.ctez_token_contract, ctez_removed)) 0mutez s.liquidity_dex_address in + let receive_subsidy = Tezos.transaction (param.to_, (s.ctez_token_contract, subsidy_to_receive)) 0mutez s.liquidity_dex_address in + let receive_tez = Tezos.transaction () (tez_to_receive * 1mutez) param.to_ in + ({s with sell_ctez = sell_ctez}, [receive_ctez; receive_subsidy; receive_tez]) + + +let min (x : nat) (y : nat) : nat = if x < y then x else y + +let clamp_nat (x : int) : nat = + match is_nat x with + | None -> 0n + | Some x -> x + +let newton_step (q : nat) (t : nat) (_Q : nat) (dq : nat): int = + (* + (3 dq⁴ + 6 dq² (q - Q)² + 8 dq³ (-q + Q) + 80 Q³ t) / (4 ((dq - q)³ + 3 (dq - q)² Q + 3 (dq - q) Q² + 21 Q³)) + todo, check that implementation below is correct + TODO: optimize the computation of [q - _Q] and other constants + (A dq^2 +B)/(C + dq(D+dq(4dq-E))) + *) + // ensures that dq < q + let dq = min dq q in + // assert q < _Q (due to clamp at [invert]) + let q_m_Q = q - _Q in + + let dq_m_q = dq - q in + let dq_m_q_sq = dq_m_q * dq_m_q in + let dq_m_q_cu = dq_m_q_sq * dq_m_q in + let _Q_sq = _Q * _Q in + let _Q_cu = _Q_sq * _Q in + + let num = 3 * dq * dq * dq * dq + 6 * dq * dq * q_m_Q * q_m_Q + 8 * dq * dq * dq * (-q_m_Q) + 80 * _Q_cu * t in + let denom = 4 * (dq_m_q_cu + 3 * dq_m_q_sq * _Q + 3 * dq_m_q * _Q_sq + 21 * _Q_cu) in + + num / denom + +let invert (q : nat) (t : nat) (_Q : nat) : nat = + (* q is the current amount, + t is the amount you want to trade + _Q is the target amount + *) + (* note that the price is generally very nearly linear, after all the worth marginal price is 1.05, so Newton + converges stupidly fast *) + let q = min q _Q in + let dq = clamp_nat (newton_step q t _Q t) in + let dq = clamp_nat (newton_step q t _Q dq) in + let dq = clamp_nat (newton_step q t _Q dq) in + let result = dq - dq / 1_000_000_000 - 1 in + match is_nat result with + | None -> failwith "trade size too small" + | Some x -> x + + +let append t1 t2 = List.fold_right (fun (x, tl) -> x :: tl) t1 t2 + +[@entry] +let tez_to_ctez (param : tez_to_ctez) (s : storage) : operation list * storage = + let update_ops, s = update_ctez_contract_if_needed s in + + let () = assert_with_error (Tezos.get_now () <= param.deadline) "deadline has passed" in + (* The amount of tez that will be bought is calculated by integrating a polynomial which is a function of the fraction u purchased over q + * the polynomial, representing the marginal price is given as (21 - 3 * u + 3 u^2 - u^3) / 20 + * again, u is the quantity of ctez purchased over q which represents this characteristic quantity of ctez in the ctez half dex.&& + * the integral of this polynomial between u = 0 and u = x / q (where x will be ctez_to_sell) is is given as + * (21 * u - 3 * u^2 / 2 + u^3 - u^4 / 4) / 20 + * or (cts(cts(cts^2-3q^2)+42 q^3))/(40q^4) *) +// let cts = ctez_to_sell in let q = s.q in +// let q2 = q * q in +// let d_tez = (cts * (cts * (cts * cts - 3 * q2) + 42 * q * q2)) / (40 * q2 * q2) in + + let t = Bitwise.shift_left (Tezos.get_amount () / 1mutez) 48n / s.target in + let ctez_to_sell = invert s.sell_ctez.total_liquidity t s._Q in + let () = assert_with_error (ctez_to_sell >= param.minCtezBought) "insufficient ctez would be bought" in + let () = assert_with_error (ctez_to_sell <= s.sell_ctez.total_liquidity) "insufficient ctez in the dex" in + // Update dex + let half_dex = s.sell_ctez in + let half_dex: half_dex = { half_dex with total_liquidity = clamp_nat (half_dex.total_liquidity - ctez_to_sell); total_proceeds = half_dex.total_proceeds + (Tezos.get_amount () / 1mutez) } in + // Transfer ctez to the buyer + let fa_contract = (Tezos.get_entrypoint "%transfer" s.ctez_token_contract : fa12_transfer contract) in + let receive_ctez = Tezos.transaction { address_from = s.liquidity_dex_address; address_to = param.to_; value = ctez_to_sell } 0mutez fa_contract in + // Deal with subsidy later + (append update_ops [receive_ctez], {s with sell_ctez = half_dex}) + + +let implicit_transfer (to_ : address) (amt : tez) : operation = + let contract = (Tezos.get_entrypoint "%default" to_ : unit contract) in + Tezos.transaction () amt contract + +let ctez_transfer (s : storage) (to_ : address) (value: nat) : operation = + let fa_contract = (Tezos.get_entrypoint "%transfer" s.ctez_token_contract : fa12_transfer contract) in + let receive_ctez = Tezos.transaction { address_from = s.liquidity_dex_address; address_to = to_; value } 0mutez fa_contract in + receive_ctez + + +[@entry] +let withdraw_for_ctez_half_dex (param : withdraw_for_ctez_half_dex) (s: storage) : operation list * storage = + // withdraw: you can withdraw x so long as x + owed < lpt * total_proceeds / total_lpt, after which owed := owed + x + // So, my thoughts on withdrawing: + // you can withdraw x, so long as x + owe < lpt * total_proceeds / total_lpt + // owe := owe + x + // 2:57 PM + // owe never decreases, it's basically a tally of everything you've ever withdrawn + // 2:57 PM + // so when you add liquidity, it's like you added all those proceeds, and then withdrew lpt * total_proceeds / total_lpt + // + // TL;DR: proceeds = tez + total owed; proceeds doesn't increase + + let owner = Tezos.get_sender () in + let half_dex = s.sell_ctez in + let liquidity_owner = Option.value_with_error "no liquidity owner" (Big_map.find_opt owner half_dex.liquidity_owners) in + let share_of_proceeds = liquidity_owner.lpt * half_dex.total_proceeds / half_dex.total_lpt in + // proceeds in tez + let amount_proceeds_withdrawn = clamp_nat (share_of_proceeds - liquidity_owner.owed) in + let share_of_subsidy = liquidity_owner.lpt * half_dex.total_subsidy / half_dex.total_lpt in + // subsidy in ctez + let amount_subsidy_withdrawn = clamp_nat (share_of_subsidy - liquidity_owner.subsidy_owed) in + // liquidity owner owes the full share of proceeds + let liquidity_owner = { liquidity_owner with owed = share_of_proceeds; subsidy_owed = share_of_subsidy } in + // update half dex + let half_dex = { half_dex with liquidity_owners = Big_map.update owner (Some liquidity_owner) half_dex.liquidity_owners } in + // do transfers + let receive_proceeds = implicit_transfer param.to_ (amount_proceeds_withdrawn * 1mutez) in + let receive_subsidy = ctez_transfer s param.to_ amount_subsidy_withdrawn in + ([receive_proceeds; receive_subsidy], {s with sell_ctez = half_dex}) + + + diff --git a/newton.mligo b/newton.mligo new file mode 100644 index 00000000..74456d32 --- /dev/null +++ b/newton.mligo @@ -0,0 +1,46 @@ +(* Functions for the isoutility curve (x + y)^8 - (x - y)^8 = k *) + +let util (x: nat) (y: nat) : nat * nat = + let plus = x + y in + let minus = x - y in + let plus_2 = plus * plus in + let plus_4 = plus_2 * plus_2 in + let plus_8 = plus_4 * plus_4 in + let plus_7 = plus_4 * plus_2 * plus in + let minus_2 = minus * minus in + let minus_4 = minus_2 * minus_2 in + let minus_8 = minus_4 * minus_4 in + let minus_7 = minus_4 * minus_2 * minus in + (* minus_7 + plus_7 should always be positive *) + (* since x >0 and y > 0, x + y > x - y and therefore (x + y)^7 > (x - y)^7 and (x + y^7 - (x - y)^7 > 0 *) + (abs (plus_8 - minus_8), 8n * (abs (minus_7 + plus_7))) + +type newton_param = {x : nat ; y : nat ; dx : nat ; dy : nat ; u : nat ; n : int} + +let rec newton (p : newton_param) : nat = + if p.n = 0 then + p.dy + else + let new_u, new_du_dy = util (p.x + p.dx) (abs (p.y - p.dy)) in + (* new_u - p.u > 0 because dy remains an underestimate *) + let dy = p.dy + abs ((new_u - p.u) / new_du_dy) in + (* dy is an underestimate because we start at 0 and the utility curve is convex *) + newton {p with dy = dy ; n = p.n - 1} + +let rec newton_dx_to_dy (x, y, dx, rounds : nat * nat * nat * int) : nat = + let xp = x + dx in + let xp2 = xp * xp in + let u, _ = util x y in + newton {x = x; y = y; dx = dx ; dy = 0n ; u = u; n = rounds} + +let margin (x: nat) (y: nat) : nat * nat = + let plus = x + y in + let minus = x - y in + let plus_2 = plus * plus in + let plus_4 = plus_2 * plus_2 in + let plus_7 = plus_4 * plus_2 * plus in + let minus_2 = minus * minus in + let minus_4 = minus_2 * minus_2 in + let minus_7 = minus_4 * minus_2 * minus in + (* plus_7 - minus_7 is always positive because it's 2 y ( 7 (x^6 + 5 x^4 y^2 + 3 x^2 y^4) + y^6) ) *) + (abs (plus_7 - minus_7), abs (plus_7 + minus_7)) (* that's (du/dx) / (du/dy) , or how many y I get for one x *) diff --git a/oven.ml b/oven.ml new file mode 100644 index 00000000..c8193a5d --- /dev/null +++ b/oven.ml @@ -0,0 +1,48 @@ +#include "oven_types.mligo" + +(fun (p , s : oven_parameter * oven_storage) -> ( + (* error codes *) + let error_WITHDRAW_CAN_ONLY_BE_CALLED_FROM_MAIN_CONTRACT = 1001n in + let error_ONLY_OWNER_CAN_DELEGATE = 1002n in + let error_CANNOT_FIND_REGISTER_DEPOSIT_ENTRYPOINT = 1003n in + let error_UNAUTHORIZED_DEPOSITOR = 1004n in + let error_SET_ANY_OFF_FIRST = 1005n in + let error_ONLY_OWNER_CAN_EDIT_DEPOSITORS = 1006n in + (match p with + (* Withdraw form the oven, can only be called from the main contract. *) + | Oven_withdraw x -> + if Tezos.sender <> s.admin then + (failwith error_WITHDRAW_CAN_ONLY_BE_CALLED_FROM_MAIN_CONTRACT : oven_result) + else + ([Tezos.transaction unit x.0 x.1], s) + (* Change delegation *) + | Oven_delegate ko -> + if Tezos.sender <> s.handle.owner then + (failwith error_ONLY_OWNER_CAN_DELEGATE : oven_result) + else ([Tezos.set_delegate ko], s) + (* Make a deposit. If authorized, this will notify the main contract. *) + | Oven_deposit -> + if Tezos.sender = s.handle.owner or ( + match s.depositors with + | Any -> true + | Whitelist depositors -> Set.mem Tezos.sender depositors + ) then + let register = ( + match (Tezos.get_entrypoint_opt "%register_deposit" s.admin : (register_deposit contract) option) with + | None -> (failwith error_CANNOT_FIND_REGISTER_DEPOSIT_ENTRYPOINT : register_deposit contract) + | Some register -> register) in + (([ Tezos.transaction {amount = Tezos.amount ; handle = s.handle} 0mutez register] : operation list), s) + else + (failwith error_UNAUTHORIZED_DEPOSITOR : oven_result) + (* Edit the set of authorized depositors. Insert tz1authorizeAnyoneToDeposit3AC7qy8Qf to authorize anyone. *) + | Oven_edit_depositor edit -> + if Tezos.sender <> s.handle.owner then + (failwith error_ONLY_OWNER_CAN_EDIT_DEPOSITORS : oven_result) + else + let depositors = (match edit with + | Allow_any allow -> if allow then Any else Whitelist (Set.empty : address set) + | Allow_account x -> let (allow, depositor) = x in (match s.depositors with + | Any -> (failwith error_SET_ANY_OFF_FIRST : depositors) + | Whitelist depositors -> Whitelist ( + if allow then Set.add depositor depositors else Set.remove depositor depositors))) in + (([] : operation list), {s with depositors = depositors})))) \ No newline at end of file diff --git a/oven.mligo b/oven.mligo index 142c9d63..2df6301b 100644 --- a/oven.mligo +++ b/oven.mligo @@ -1,6 +1,6 @@ #include "oven_types.mligo" -let create_oven (delegate : key_hash option) (amnt : tez) (storage : oven_storage) = Tezos.create_contract +let originate_oven (delegate : key_hash option) (amnt : tez) (storage : oven_storage) = Tezos.create_contract (* Contract code for an oven *) (fun (p , s : oven_parameter * oven_storage) -> ( (* error codes *) @@ -13,32 +13,32 @@ let create_oven (delegate : key_hash option) (amnt : tez) (storage : oven_storag (match p with (* Withdraw form the oven, can only be called from the main contract. *) | Oven_withdraw x -> - if Tezos.sender <> s.admin then + if Tezos.get_sender () <> s.admin then (failwith error_WITHDRAW_CAN_ONLY_BE_CALLED_FROM_MAIN_CONTRACT : oven_result) else ([Tezos.transaction unit x.0 x.1], s) (* Change delegation *) | Oven_delegate ko -> - if Tezos.sender <> s.handle.owner then + if Tezos.get_sender () <> s.handle.owner then (failwith error_ONLY_OWNER_CAN_DELEGATE : oven_result) else ([Tezos.set_delegate ko], s) (* Make a deposit. If authorized, this will notify the main contract. *) | Oven_deposit -> - if Tezos.sender = s.handle.owner or ( + if Tezos.get_sender () = s.handle.owner or ( match s.depositors with | Any -> true - | Whitelist depositors -> Set.mem Tezos.sender depositors + | Whitelist depositors -> Set.mem (Tezos.get_sender ()) depositors ) then let register = ( - match (Tezos.get_entrypoint_opt "%register_deposit" s.admin : (register_deposit contract) option) with - | None -> (failwith error_CANNOT_FIND_REGISTER_DEPOSIT_ENTRYPOINT : register_deposit contract) + match (Tezos.get_entrypoint_opt "%register_deposit" s.admin : (register_oven_deposit contract) option) with + | None -> (failwith error_CANNOT_FIND_REGISTER_DEPOSIT_ENTRYPOINT : register_oven_deposit contract) | Some register -> register) in - (([ Tezos.transaction {amount = Tezos.amount ; handle = s.handle} 0mutez register] : operation list), s) + (([ Tezos.transaction {amount = Tezos.get_amount () ; handle = s.handle} 0mutez register] : operation list), s) else (failwith error_UNAUTHORIZED_DEPOSITOR : oven_result) (* Edit the set of authorized depositors. Insert tz1authorizeAnyoneToDeposit3AC7qy8Qf to authorize anyone. *) | Oven_edit_depositor edit -> - if Tezos.sender <> s.handle.owner then + if Tezos.get_sender () <> s.handle.owner then (failwith error_ONLY_OWNER_CAN_EDIT_DEPOSITORS : oven_result) else let depositors = (match edit with diff --git a/oven_types.ml b/oven_types.ml new file mode 100644 index 00000000..f69dccc4 --- /dev/null +++ b/oven_types.ml @@ -0,0 +1,31 @@ +#if !OVEN_TYPES +#define OVEN_TYPES + +type edit = + | Allow_any of bool + | Allow_account of bool * address + +type oven_parameter = + | Oven_delegate of (key_hash option) + | [@annot:default] Oven_deposit + | Oven_edit_depositor of edit + | Oven_withdraw of tez * (unit contract) + +type depositors = + | Any + | Whitelist of address set + +type oven_handle = [@layout:comb] {id : nat ; owner : address} +type register_deposit = [@layout:comb] { handle : oven_handle ; amount : tez } + + +type oven_storage = { + admin : address (* vault admin contract *) ; + handle : oven_handle (* owner of the oven *) ; + depositors : depositors (* who can deposit in the oven *) ; + } +type oven_result = (operation list) * oven_storage + + + +#endif \ No newline at end of file diff --git a/oven_types.mligo b/oven_types.mligo index f69dccc4..b593d6e4 100644 --- a/oven_types.mligo +++ b/oven_types.mligo @@ -16,7 +16,7 @@ type depositors = | Whitelist of address set type oven_handle = [@layout:comb] {id : nat ; owner : address} -type register_deposit = [@layout:comb] { handle : oven_handle ; amount : tez } +type register_oven_deposit = [@layout:comb] { handle : oven_handle ; amount : tez } type oven_storage = { diff --git a/stdctez.mligo b/stdctez.mligo new file mode 100644 index 00000000..37307371 --- /dev/null +++ b/stdctez.mligo @@ -0,0 +1,28 @@ +// Various helpful stdlib extensions for ctez + +type 'a with_operations = operation list * 'a + +[@inline] +let clamp_nat (x : int) : nat = + match is_nat x with + | None -> 0n + | Some x -> x + +[@inline] +let min (x : nat) (y : nat) : nat = if x < y then x else y + +[@inline] +let ceildiv (numerator : nat) (denominator : nat) : nat = abs ((- numerator) / (int denominator)) + +module Float48 = struct + type t = int + + // TODO +end + +module List = struct + include List + + [@inline] + let append t1 t2 = fold_right (fun (x, tl) -> x :: tl) t1 t2 +end \ No newline at end of file diff --git a/tests/test.mligo b/tests/test.mligo new file mode 100644 index 00000000..ee7998b7 --- /dev/null +++ b/tests/test.mligo @@ -0,0 +1,380 @@ +(* This is a testing framework for ctez *) + +(* ============================================================================= + * Contract Templates + * ============================================================================= *) + +#include "../fa12.mligo" +let main_fa12 = main +type fa12_storage = storage +type fa12_parameter = parameter +type fa12_result = result + +let main_lqt = main +type lqt_storage = storage +type lqt_parameter = parameter +type lqt_result = result + +#include "../ctez.mligo" +let main_ctez = main +type ctez_storage = storage +type ctez_parameter = parameter +type ctez_result = result + +#include "../cfmm.mligo" +let main_cfmm = main +type cfmm_storage = storage +type cfmm_parameter = parameter +type cfmm_result = result + +#include "../test_params.mligo" + +(* ============================================================================= + * Some Aux Functions + * ============================================================================= *) + +// Returns the price dy/dx of the isoutility function, i.e. a map by multiplication ∆x => ∆y, at a given point (x,y) +let price_cash_to_token (target : nat) (cash : nat) (token : nat) : nat = + let (x,y) = (cash, token) in + let a = target in + let ax2 = x * x * a * a in + let by2 = Bitwise.shift_left (y * y) 96n in + let num = y * (3n * ax2 + by2) in + let denom = x * (ax2 + 3n * by2) in + num/denom + +// The isoutility function used +let isoutility (target, cash, token : nat * nat * nat) : nat = + let x = cash in + let y = token in + let a = target in + let a2 = a * a in + let ax2 = a2 * x * x in + let by2 = Bitwise.shift_left (y * y) 96n in + (Bitwise.shift_right (abs((a * x * y) * (ax2 + by2) / (2 * a2))) 48n) + + +(* ============================================================================= + * Generic Setup that Initiates All Contracts + * ============================================================================= *) + +let init_contracts (alice_bal : nat option) (bob_bal : nat option) (init_lqt : nat option) (init_total_supply : nat option) + (init_token_pool : nat option) (init_cash_pool : nat option) (init_target : nat option) + (init_drift : int option) (last_drift_update : timestamp option) (const_fee : (nat * nat) option) + (pending_pool_updates : nat option) (init_ovens : (oven_handle, oven) big_map option) = + // set time + let now = ("2000-01-01t10:10:10Z" : timestamp) in + + // set defaults for optional args + let alice_bal = match alice_bal with | None -> 100n | Some b -> b in + let bob_bal = match bob_bal with | None -> 100n | Some b -> b in + let init_lqt = match init_lqt with | None -> 10n | Some l -> l in + let init_total_supply = match init_total_supply with | None -> 1_000_000_000_000_000n | Some s -> s in + let init_token_pool = match init_token_pool with | None -> 1_000_000_000_000n | Some t -> t in // muctez, e.g. 1_000_000 ctez + let init_cash_pool = match init_cash_pool with | None -> 1_000_000_000_000n | Some c -> c in // mutez, e.g. 1_000_000 tez + let init_target = match init_target with | None -> (Bitwise.shift_left 1n 48n) | Some t -> t in // default target is 1 (Bitwise.shift_left 1n 48n) + let init_drift = match init_drift with | None -> 0 | Some d -> d in + let last_drift_update = match last_drift_update with | None -> now | Some t -> t in + let const_fee = match const_fee with | None -> (1000n, 1000n)| Some f -> f in // no default fee + let pending_pool_updates = match pending_pool_updates with | None -> 0n | Some p -> p in + let init_ovens = match init_ovens with | None -> (Big_map.empty : (oven_handle, oven) big_map) | Some o -> o in + + // generate some implicit addresses + let reset_state_unit = Test.reset_state 5n ([] : nat list) in + let (addr_alice, addr_bob, addr_lqt, addr_dummy, addr_admin) = + (Test.nth_bootstrap_account 0, Test.nth_bootstrap_account 1, Test.nth_bootstrap_account 2, + Test.nth_bootstrap_account 3, Test.nth_bootstrap_account 4) + in + let null_address = ("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU" : address) in + + // ctez fa12 contract + let fa12_init_storage : fa12_storage = + { + tokens = ( Big_map.literal [(addr_alice, alice_bal); (addr_bob, bob_bal)] : (address, nat) big_map); + allowances = (Big_map.empty : allowances); + admin = addr_admin; + total_supply = init_total_supply; + } in + let (typed_addr_fa12, program_fa12, size_fa12) = Test.originate main_fa12 fa12_init_storage 0tez in + + // lqt fa12 contract + let lqt_init_storage : fa12_storage = { + tokens = ( Big_map.literal [(addr_lqt, init_lqt);] : (address, nat) big_map); + allowances = (Big_map.empty : allowances); + admin = addr_admin; + total_supply = init_total_supply; + } in + let (typed_addr_lqt, program_lqt, size_lqt) = Test.originate main_lqt lqt_init_storage 0tez in + + // ctez contract + let ctez_init_storage : ctez_storage = { + ovens = init_ovens; + target = init_target; + drift = init_drift; + last_drift_update = last_drift_update; + ctez_fa12_address = null_address; + cfmm_address = null_address; + } in + let (typed_addr_ctez, program_ctez, size_ctez) = + Test.originate main_ctez ctez_init_storage 0tez + in + + // initiate the cfmm contract + let cfmm_init_storage : cfmm_storage = { + tokenPool = init_token_pool ; + cashPool = init_cash_pool ; + lqtTotal = init_lqt ; + target = init_target ; + const_fee = const_fee; + ctez_address = Tezos.address (Test.to_contract typed_addr_ctez) ; + pendingPoolUpdates = 0n ; + tokenAddress = Tezos.address (Test.to_contract typed_addr_fa12) ; + lqtAddress = Tezos.address (Test.to_contract typed_addr_lqt) ; +#if ORACLE + lastOracleUpdate = now ; + consumerEntrypoint = Tezos.address (Test.to_entrypoint "cfmm_price" typed_addr_ctez : (nat * nat) contract) ; +#endif + } in + let (typed_addr_cfmm, program_cfmm, size_cfmm) = Test.originate main_cfmm cfmm_init_storage (1mutez * init_cash_pool) in + + // update ctez's storage using its set_addresses entrypoints + let entrypoint_set_addresses = + ((Test.to_entrypoint "set_addresses" typed_addr_ctez) : set_addresses contract) in + let untyped_addr_cfmm = Tezos.address (Test.to_contract typed_addr_cfmm) in + let untyped_addr_fa12 = Tezos.address (Test.to_contract typed_addr_fa12) in + let txndata_set_addresses : set_addresses = { + cfmm_address=untyped_addr_cfmm; + ctez_fa12_address=untyped_addr_fa12; + } in + let update_ctez_addresses = Test.transfer_to_contract_exn entrypoint_set_addresses txndata_set_addresses 0tez in + + // mint tokens in the amount of init_token_pool for the cfmm contract + let admin_source = Test.set_source addr_admin in + let addr_cfmm = Tezos.address (Test.to_contract typed_addr_cfmm) in + let entrypoint_mint : mintOrBurn contract = Test.to_entrypoint "mintOrBurn" typed_addr_fa12 in + let txndata_mint : mintOrBurn = { quantity=int(init_token_pool) ; target=addr_cfmm } in + let txn_mint = Test.transfer_to_contract_exn entrypoint_mint txndata_mint 0tez in + + (typed_addr_cfmm, typed_addr_ctez, typed_addr_fa12, typed_addr_lqt, + addr_alice, addr_bob, addr_lqt, addr_dummy, addr_admin) + +(* ============================================================================= + * Tests + * ============================================================================= *) + +(******** STANDARD SETUP ********) +let test_setup = + // default init setup + let (typed_addr_cfmm, typed_addr_ctez, typed_addr_fa12, typed_addr_lqt, + addr_alice, addr_bob, addr_lqt, addr_dummy, addr_admin) = + init_contracts + (None : nat option) (* alice_bal *) + (None : nat option) (* bob_bal *) + (None : nat option) (* init_lqt *) + (None : nat option) (* init_total_supply *) + (None : nat option) (* init_token_pool *) + (None : nat option) (* init_cash_pool *) + (None : nat option) (* init_target *) + (None : int option) (* init_drift *) + (None : timestamp option) (* last_drift_update *) + (None : (nat * nat) option) (* const_fee *) + (None : nat option) (* pending_pool_updates *) + (None : (oven_handle, oven) big_map option) (* init_ovens *) + in + // make sure ctez's storage is as expected + let untyped_addr_fa12 = Tezos.address (Test.to_contract typed_addr_fa12) in + let untyped_addr_cfmm = Tezos.address (Test.to_contract typed_addr_cfmm) in + let expected_ctez_storage : ctez_storage = { + ovens = (Big_map.empty : (oven_handle, oven) big_map); + target = 1n; // TODO go up and down 5% in 0.1% increments + drift = 0; // TODO should this actually be a pair? + last_drift_update = ("2000-01-01t10:10:10Z" : timestamp); // 5 mins (300 sec), this should vary I think + ctez_fa12_address = untyped_addr_fa12; + cfmm_address = untyped_addr_cfmm; + } in + let actual_ctez_storage = Test.get_storage typed_addr_ctez in + // assertions to verify storage is as expected + ( + assert (expected_ctez_storage.cfmm_address = actual_ctez_storage.cfmm_address), + assert (expected_ctez_storage.ctez_fa12_address = actual_ctez_storage.ctez_fa12_address) + ) + // (* when Bytes works in test *) assert (Bytes.pack expected_ctez_storage = Bytes.pack actual_ctez_storage ) + + +(******** DIFFERENCE EQUATIONS ********) +(** cash to token **) +let trade_cash_to_token_test (x, y, dx, target, rounds, const_fee : nat * nat * nat * nat * int * (nat * nat)) = + let (typed_addr_cfmm, typed_addr_ctez, typed_addr_fa12, typed_addr_lqt, + addr_alice, addr_bob, addr_lqt, addr_dummy, addr_admin) = + init_contracts + (Some 0n : nat option) (* alice_bal *) + (Some 0n : nat option) (* bob_bal *) + (None : nat option) (* init_lqt *) + (None : nat option) (* init_total_supply *) + (Some y : nat option) (* init_token_pool *) + (Some x : nat option) (* init_cash_pool *) + (Some target : nat option) (* init_target *) + (None : int option) (* init_drift *) + (None : timestamp option) (* last_drift_update *) + (Some const_fee : (nat * nat) option) (* const_fee *) + (None : nat option) (* pending_pool_updates *) + (None : (oven_handle, oven) big_map option) (* init_ovens *) + in + + // get the TokenToCash entrypoint + let alice_source = Test.set_source addr_alice in + let trade_entrypoint : cash_to_token contract = + Test.to_entrypoint "cashToToken" typed_addr_cfmm in + let trade_amt = (dx * 1mutez) in + let trade_data : cash_to_token = { + to_ = addr_alice; + minTokensBought = 0n; + deadline = ("3000-01-01t10:10:10Z" : timestamp); + rounds = rounds; + } in + let alice_balance_old = Test.get_balance addr_alice in + let alice_trade = + (Test.transfer_to_contract_exn trade_entrypoint trade_data trade_amt) in + + // alice should trade 500ctez for tez + let ctez_fa12_storage = Test.get_storage typed_addr_fa12 in + let ctez_token_balances = ctez_fa12_storage.tokens in + let (fee_num, fee_denom) = const_fee in + + match (Big_map.find_opt addr_alice ctez_token_balances) with + | None -> (failwith "Incomplete Cash to Token Transfer" : unit) + | Some bal -> assert (bal = fee_num * (trade_dcash_for_dtoken x y dx target rounds) / fee_denom) + // (*debug mode*) | None -> (failwith "Incomplete Cash to Token Transfer" : nat * nat) + // (*debug mode*) | Some bal -> (bal, fee_num * (trade_dcash_for_dtoken x y dx target rounds) / fee_denom) + +let test_cash_to_token = + let test_result = List.map trade_cash_to_token_test trade_params in + () // if it reaches this, everything passed + // (*debug mode*) test_result + +(** token to cash **) +let trade_token_to_cash_test (x, y, dy, target, rounds, const_fee : nat * nat * nat * nat * int * (nat * nat)) = + let alice_initial_bal = dy in // in muctez + let (typed_addr_cfmm, typed_addr_ctez, typed_addr_fa12, typed_addr_lqt, + addr_alice, addr_bob, addr_lqt, addr_dummy, addr_admin) = + init_contracts + (Some alice_initial_bal : nat option) (* alice_bal *) + (Some 0n : nat option) (* bob_bal *) + (None : nat option) (* init_lqt *) + (None : nat option) (* init_total_supply *) + (Some y : nat option) (* init_token_pool *) + (Some x : nat option) (* init_cash_pool *) + (Some target : nat option) (* init_target *) + (None : int option) (* init_drift *) + (None : timestamp option) (* last_drift_update *) + (Some const_fee : (nat * nat) option) (* const_fee *) + (None : nat option) (* pending_pool_updates *) + (None : (oven_handle, oven) big_map option) (* init_ovens *) + in + // get the TokenToCash and Approve entrypoints + let alice_source = Test.set_source addr_alice in + let alice_txn_amt = alice_initial_bal in + let addr_cfmm = Tezos.address (Test.to_contract typed_addr_cfmm) in + let addr_cfmm_balance_old = Test.get_balance addr_cfmm in + let bob_balance_old = Test.get_balance addr_bob in + + // trade entrypoint + let trade_entrypoint : token_to_cash contract = + Test.to_entrypoint "tokenToCash" typed_addr_cfmm in + let trade_data : token_to_cash = { + to_ = addr_bob; // send to bob so that gas costs don't factor into the change in balance + tokensSold = alice_txn_amt; // in muctez + minCashBought = 0n; + deadline = ("3000-01-01t10:10:10Z" : timestamp); + rounds = rounds; + } in + + // approve entrypoint + let approve_entrypoint : approve contract = + Test.to_entrypoint "approve" typed_addr_fa12 in + let approve_data : approve = { + spender = addr_cfmm ; + value = alice_txn_amt ; + } in + + // execute and test + let alice_approve = + (Test.transfer_to_contract_exn approve_entrypoint approve_data 0tez) in + let alice_trade = + (Test.transfer_to_contract_exn trade_entrypoint trade_data 0tez) in + + let addr_cfmm_balance = Test.get_balance addr_cfmm in + let bob_balance = Test.get_balance addr_bob in + let bob_balance_delta = bob_balance / 1mutez - bob_balance_old / 1mutez in + let (fee_num, fee_denom) = const_fee in + assert (abs bob_balance_delta = fee_num * (trade_dtoken_for_dcash x y dy target rounds) / fee_denom) + // (*debug mode*) (abs bob_balance_delta, fee_num * (trade_dtoken_for_dcash x y dy target rounds) / fee_denom) + +let test_token_to_cash = + let test_result = List.map trade_token_to_cash_test trade_params in + () // if it reaches this, everything passed + //(*debug mode*) test_result + + +(******** DIRECTIVES ********) +let test_directives = () +// Tests compilation under different directives (may not be feasible in this framework) + + +(******** DRIFT ********) + (* This is a minimal e.g., as timestamp arithmetic does not currently work in ligo test *) +// TODO : When time works, +// let test_drift (init_cash : nat) (init_token : nat) (init_target : nat) (time_delta : int) = +#if ORACLE +let test_drift (init_cash : nat) (init_token : nat) (init_target : nat) (now : timestamp) (later : timestamp) = + // some failing conditions + (* if (time_delta < 0) then (failwith "time_delta MUST BE NONNEGATIVE" : unit) else *) + + // init contracts + let (typed_addr_cfmm, typed_addr_ctez, typed_addr_fa12, typed_addr_lqt, + addr_alice, addr_bob, addr_lqt, addr_dummy, addr_admin) = + init_contracts + (None : nat option) (* alice_bal *) + (None : nat option) (* bob_bal *) + (None : nat option) (* init_lqt *) + (None : nat option) (* init_total_supply *) + (Some init_token : nat option) (* init_token_pool *) + (Some init_cash : nat option) (* init_cash_pool *) + (Some init_target : nat option) (* init_target *) + (None : int option) (* init_drift *) + (Some now : timestamp option) (* last_drift_update *) + (None : (nat * nat) option) (* const_fee *) + (None : nat option) (* pending_pool_updates *) + (None : (oven_handle, oven) big_map option) (* init_ovens *) + in + + // update time + let time_has_elapsed = Test.set_now later in + + // execute a transaction, which will trigger a drift update + let alice_source = Test.set_source addr_alice in + let trade_entrypoint : cash_to_token contract = + Test.to_entrypoint "cashToToken" typed_addr_cfmm in + let trade_amt = 100_000_000mutez in // could be anything + let trade_data : cash_to_token = { + to_ = addr_alice; + minTokensBought = 0n; + deadline = ("3000-01-01t10:10:10Z" : timestamp); + rounds = 4; + } in + let alice_trade = + (Test.transfer_to_contract_exn trade_entrypoint trade_data trade_amt) in + + // check the drift + let ctez_storage = Test.get_storage typed_addr_ctez in + let actual_drift = ctez_storage.drift in + + // compute expected drift + let expected_drift = 0n (* TODO: calculation here *) in + + assert (actual_drift = expected_drift) + + // Checks that drift and target grew at expected rate after x mins + // 5 mins, or 300 secs will be default + // target should go up and down 5% in 0.1% increments +#endif diff --git a/tests/test_params.mligo b/tests/test_params.mligo new file mode 100644 index 00000000..ebaa25be --- /dev/null +++ b/tests/test_params.mligo @@ -0,0 +1,23 @@ +// params: x y dx target rounds *OR* y x dy target rounds +// params: cash token dcash target rounds *OR* token cash dtoken target rounds +let trade_params : (nat * nat * nat * nat * int * (nat * nat)) list = [ + (1_000_000_000_000n, 1_000_000_000_000n, 100_000_000n, (Bitwise.shift_left 1n 48n), 4, (1_000n, 1_000n)) ; // defaults + (1_000_000_000_000n, 10_000_000_000_000n, 100_000_000n, (Bitwise.shift_left 2n 48n), 4, (1_000n, 1_000n)) ; + // TODO : populate +] + +// output: tokens +let expected_cash_to_token : nat list = [ + 99_999_999n ; +] + +// output: cash +let expected_token_to_cash : nat list = [ + 99_999_999n ; +] + + +// fee variation +let fees : (nat * nat) list = [ + (1_000n,1_000n) ; +] \ No newline at end of file diff --git a/tests/unit_test.mligo b/tests/unit_test.mligo new file mode 100644 index 00000000..35daf7f5 --- /dev/null +++ b/tests/unit_test.mligo @@ -0,0 +1,50 @@ +(* This is a unit testing framework for ctez *) + +(* ============================================================================= + * Contract Templates + * ============================================================================= *) + +#include "../fa12.mligo" +let main_fa12 = main +type fa12_storage = storage +type fa12_parameter = parameter +type fa12_result = result + +let main_lqt = main +type lqt_storage = storage +type lqt_parameter = parameter +type lqt_result = result + +#include "../ctez.mligo" +let main_ctez = main +type ctez_storage = storage +type ctez_parameter = parameter +type ctez_result = result + +#include "../cfmm.mligo" +let main_cfmm = main +type cfmm_storage = storage +type cfmm_parameter = parameter +type cfmm_result = result + +#include "test_params.mligo" + +(* ============================================================================= + * Contract Templates + * ============================================================================= *) + +(* Newton tests *) +let test_newton = + newton_dx_to_dy (1_000_000n, 1_000_000n, 100n, 0n, (1000n, 1000n), 4) + + +(* TODO : test these against the python code *) +let test_dtoken_to_dcash = + let trade = fun (x, y, dx, target, rounds, (fee_num, fee_denom) : nat * nat * nat * nat * int * (nat * nat)) -> fee_num * (trade_dtoken_for_dcash x y dx target rounds) / fee_denom in + let traded = List.map trade trade_params in + traded //assert (traded = expected_token_to_cash) + +let test_dcash_to_dtoken = + let trade = fun (x, y, dy, target, rounds, (fee_num, fee_denom) : nat * nat * nat * nat * int * (nat * nat)) -> fee_num * trade_dcash_for_dtoken x y dy target rounds / fee_denom in + let traded = List.map trade trade_params in + traded //assert (traded = expected_token_to_cash) \ No newline at end of file