Skip to content

Commit

Permalink
Tests and fixes for Torus liquidity pool edge cases. Fixes #517
Browse files Browse the repository at this point in the history
  • Loading branch information
mikera committed Dec 4, 2024
1 parent 900547c commit a5efcc6
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 43 deletions.
77 changes: 34 additions & 43 deletions convex-core/src/main/cvx/convex/torus/exchange.cvx
Original file line number Diff line number Diff line change
Expand Up @@ -46,46 +46,47 @@
(def token-balance 0)

(defn -qc
^{:doc {:description "Quantity check."}
:private? true}
[q]
(cond (int? q) q ;; base case, quantity should always be an integer
(nil? q) 0
(fail :ARGUMENT "Invalid token quantity")))

;; Add liquidity expects an amount of tokens, and and offer of Convex Coins
(defn add-liquidity
^{:callable true}
[amount]
(let [;; Amount of tokens deposited.
amount (-qc amount)

;; Price of token in CVX (double), nil if no current liquidity.
;; Price of token in CVM (double), nil if no current liquidity.
price (price)
initial-cvx-balance *balance*

initial-cvm-balance *balance*

;; calculate initial liquidity
liquidity (sqrt (* (double initial-cvm-balance) token-balance))
has-liquidity (> liquidity 0)

;; Amount of CVX required (all if initial deposit). Note Long range required
cvx (core/accept (if price
(long (* price amount))
*offer*))
cvm (core/accept (if has-liquidity
(long (* price amount))
*offer*))
;; Ensures tokens are transferred from caller to market actor.
_ (asset/accept *caller* token amount)

;; Compute new total balances for actor
new-token-balance (+ token-balance amount)

new-token-balance (+ token-balance amount)
new-liquidity (sqrt (* (double *balance*) new-token-balance))

;; Compute number of new shares for depositor = increase in liquidity (%) * current total shares.
;; If no current liquidity just initialise with the geometric mean of amounts deposited.
delta (if (> supply 0)
(let [;; Initial size of liquidity pool (geometric mean).
liquidity (sqrt (* (double initial-cvx-balance) token-balance))
new-liquidity (sqrt (* (double *balance*) new-token-balance))]
(int (* (- new-liquidity liquidity) (/ supply liquidity))))
(int (sqrt (* (double amount) cvx))))]
delta (if has-liquidity
(int (* (- new-liquidity liquidity) (/ supply liquidity)))
(int new-liquidity))]

;; Perform updates to reflect new holdings of liquidity pool shares and total token balance (all longs)
(set-holding *caller* (+ delta (or (get-holding *caller*) 0)))
(def supply (+ supply delta)) ;; Note Supply set up by fungible/build-token
(def token-balance new-token-balance)
(set! supply (+ supply delta)) ;; Note Supply set up by fungible/build-token
(set! token-balance new-token-balance)
delta))


Expand Down Expand Up @@ -188,13 +189,13 @@

(defn price
^{:callable true}
;; Price is CVX amount per token, or nil if there are no tokens in liquidity pool.
;; Price is CVM amount per token, or nil if there are no tokens in liquidity pool.
[]

(when (> token-balance
0)
(/ (double *balance*)
token-balance)))
(let [tok token-balance
cvm *balance*]
(cond (and (> tok 0) (> cvm 0))
(/ cvm
tok))))


(defn sell-cvx
Expand Down Expand Up @@ -296,18 +297,17 @@
shares (-qc shares)
;; Shares of holder.
own-holding (or (get-holding *caller*) 0)
_ (assert (<= 0
shares
own-holding))

;; Check withrawal amount is valid
_ (assert (<= 0 shares own-holding))

proportion (if (> supply
0)
(/ (double shares)
supply)
0.0)
coin-refund (int (* proportion
*balance*))
token-refund (int (* proportion
token-balance))]
coin-refund (int (* proportion *balance*))
token-refund (int (* proportion token-balance))]
;; SECURITY:
;; 1. Update balances then transfer coins first. Risk of re-entrancy attack if transfers are made while
;; this actor is in an inconsistent state so we MUST do accounting first.
Expand All @@ -331,8 +331,6 @@
token-refund)
shares)))])



(defn create-market

^{:callable true
Expand Down Expand Up @@ -383,7 +381,7 @@
([token token-amount]
(if-let [p (price token)]
(recur token token-amount (inc (int (* p token-amount))))
(fail "No liquidity to set market price yet")))
(fail "No liquidity")))

([token token-amount cvx-amount]
(let [market (create-market token)]
Expand All @@ -400,7 +398,7 @@
[of-token amount with-token]

(let [market (or (get-market of-token)
(fail (str "Torus: market does not exist for token: " of-token)))
(fail :STATE (str "No Torus market for token: " of-token)))
cvx-amount (or (call market (buy-tokens-quote amount))
(fail :LIQUIDITY "No liquidity available to buy token"))
sold (buy-cvx with-token cvx-amount)]
Expand All @@ -410,7 +408,7 @@


(defn buy-cvx
^{:doc {:description "Buy and amount of CVX using a given token."
^{:doc {:description "Buy CVM using a given token."
:signature [{:params [token amount]}]}}

[token amount]
Expand Down Expand Up @@ -475,8 +473,6 @@
(/ price.cvx
price.currency))))



(defn sell
^{:doc {:description "Sells a given amount of a fungible token in exchange for another token"
:signature [{:params [of-token amount with-token]}]}}
Expand All @@ -486,8 +482,6 @@
(let [cvx-amount (sell-tokens of-token amount)]
(sell-cvx with-token cvx-amount)))



(defn sell-cvx
^{:doc {:description "Sell an amount of CVX for a given token."
:signature [{:params [token amount]}]}}
Expand All @@ -499,8 +493,6 @@
amount
(sell-cvx amount))))



(defn sell-quote
^{:doc {:description "Get a quote for selling an amount of a token, in CVX or optional swap token."
:signature [{:params [of-token amount]}
Expand Down Expand Up @@ -528,7 +520,6 @@
(asset/offer market [token amount])
(call market (sell-tokens amount))))


(defn withdraw-liquidity
^{:doc {:description "Withdraw an amount of liquidity for a token."
:signature [{:params [token shares]}]}}
Expand Down
39 changes: 39 additions & 0 deletions convex-core/src/test/java/convex/actors/TorusTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import convex.core.cvm.Context;
import convex.core.data.AVector;
import convex.core.data.prim.CVMDouble;
import convex.core.data.prim.CVMLong;
import convex.core.lang.ACVMTest;
import convex.core.lang.RT;
import convex.lib.AssetTester;
Expand Down Expand Up @@ -198,6 +199,44 @@ public class TorusTest extends ACVMTest {
});
}

@Test public void testLiquidityZeroTokens() {
Context ctx=context();
ctx= exec(ctx,"(def BROK (deploy (@convex.fungible/build-token {:supply 1000000})))");

ctx= exec(ctx,"(def BM (call torus (create-market BROK)))");
assertNull(eval(ctx,"(torus/price BROK)"));

ctx= exec(ctx,"(torus/add-liquidity BROK 0 1000)");
assertEquals(CVMLong.ZERO,ctx.getResult());
assertNull(eval(ctx,"(torus/price BROK)"));

ctx= exec(ctx,"(torus/add-liquidity BROK 1000 1000)");
assertEquals(CVMDouble.create(2.0),eval(ctx,"(torus/price BROK)"));

CVMLong E_SHARES=CVMLong.create(1414); // 1000 * sqrt(2)
assertEquals(E_SHARES,eval(ctx,"(asset/balance BM *address*)"));
}

@Test public void testLiquidityZeroCVM() {
// Bug fix for #517, thanks Ash!
Context ctx=context();
ctx= exec(ctx,"(def BROK (deploy (@convex.fungible/build-token {:supply 1000000})))");

ctx= exec(ctx,"(def BM (call torus (create-market BROK)))");
assertNull(eval(ctx,"(torus/price BROK)"));

ctx= exec(ctx,"(torus/add-liquidity BROK 1000 0)");
assertEquals(CVMLong.ZERO,ctx.getResult());
assertNull(eval(ctx,"(torus/price BROK)"));

ctx= exec(ctx,"(torus/add-liquidity BROK 0 1000)");
CVMLong E_SHARES=CVMLong.create(1000);
assertEquals(E_SHARES,ctx.getResult());
assertEquals(CVMDouble.ONE,eval(ctx,"(torus/price BROK)"));
assertEquals(E_SHARES,eval(ctx,"(asset/balance BM *address*)"));

}

@Test public void testTorusAPI() {
Context ctx=context();

Expand Down

0 comments on commit a5efcc6

Please sign in to comment.