diff --git a/convex-core/src/main/cvx/convex/lab/account-recycler/actor.cvx b/convex-core/src/main/cvx/convex/lab/account-recycler/actor.cvx new file mode 100644 index 0000000000..f7479dda79 --- /dev/null +++ b/convex-core/src/main/cvx/convex/lab/account-recycler/actor.cvx @@ -0,0 +1,174 @@ +'convex.account-recycler + +;; Account Recycling Contract for Convex +;; Enables buying and selling of accounts with bonding curve pricing +;; +;; Addresses can be recycled: cleared of old state and made available for new owners. +;; Uses a bonding curve to price accounts based on availability. + +;;;;;;;;;; State + +(def *inventory* {}) +;; Map of Address -> {:status :available/:reserved/:sold, :price } + +(def *available-count* 0) +(def *total-recycled* 0) + +;; Bonding curve parameters +(def BASE-PRICE 1000) ;; Base price in Copper (smallest unit) +(def PRICE-ELASTICITY 500) ;; Price increase per account (basis points) +(def MIN-PRICE 100) ;; Minimum account price +(def MAX-PRICE 1000000) ;; Maximum account price + +;;;;;;;;;; Internal helpers + +(defn -self + ^{:private? true} + [] + *address*) + +(defn -price-for-available + "Calculate current buy price based on available accounts (bonding curve). + Fewer available = higher price." + [] + (let [available (count (filter (fn [[_ v]] (= (:status v) :available)) *inventory*))] + (if (<= available 0) + MAX-PRICE + (let [ratio (/ (* PRICE-ELASTICITY 1000) (max available 1)) + price (+ BASE-PRICE ratio)] + (min MAX-PRICE (max MIN-PRICE price)))))) + +(defn -sell-price + "Calculate sell price (typically 80-90% of buy price to create spread)" + [buy-price] + (max MIN-PRICE (int (* buy-price 85) 100))) + +(defn -clear-account! + "Clear an account for recycling: undef all defs, reset controller and key" + [addr] + (let [env (call addr *env*)] + ;; Undef all user-defined symbols + (for [sym (keys env)] + (when-not (= (namespace sym) "convex.core") + (eval-as addr `(undef ~sym))))) + ;; Reset controller to nil (no controller = open for new owner) + (eval-as addr '(set-controller nil)) + ;; Note: set-key requires transaction signing, handled by new owner + :cleared) + +;;;;;;;;;; Public API + +(defn price + ^{:callable true + :doc {:description "Get current buy price for an account from the recycling market"}} + [] + (-price-for-available)) + +(defn sell-price + ^{:callable true + :doc {:description "Get the price the market will pay to buy back an account"}} + [] + (-sell-price (-price-for-available))) + +(defn inventory + ^{:callable true + :doc {:description "Get full inventory map"}} + [] + *inventory*) + +(defn available-count + ^{:callable true + :doc {:description "Count of available accounts"}} + [] + (count (filter (fn [[_ v]] (= (:status v) :available)) *inventory*))) + +(defn deposit + ^{:callable true + :doc {:description "Deposit an account to the recycling market. + Caller must control the account being deposited. + Account will be cleared and made available for purchase."}} + [addr] + (let [addr (address addr)] + ;; Verify caller controls the account + (or (= *caller* addr) + (fail :TRUST "You can only deposit accounts you control")) + ;; Clear the account + (-clear-account! addr) + ;; Add to inventory + (set! *inventory* (assoc *inventory* addr {:status :available :deposited-by *caller*})) + (set! *available-count* (inc *available-count*)) + (set! *total-recycled* (inc *total-recycled*)) + (log "DEPOSIT" addr *caller*) + addr)) + +(defn buy + ^{:callable true + :doc {:description "Buy an available account from the recycling market. + Payment is taken from caller. Account is transferred to caller."}} + [] + (let [current-price (-price-for-available) + available (filter (fn [[_ v]] (= (:status v) :available)) *inventory*)] + (or (not (empty? available)) (fail :STATE "No accounts available for purchase")) + (or (>= (balance *caller*) current-price) (fail :FUNDS "Insufficient funds")) + + ;; Take first available account + (let [[addr info] (first available)] + ;; Transfer payment to contract + (transfer *address* current-price) + + ;; Clear account and set new owner as controller + (eval-as addr `(set-controller ~*caller*)) + + ;; Update inventory + (set! *inventory* (assoc *inventory* addr {:status :sold :sold-to *caller* :price current-price})) + (set! *available-count* (dec *available-count*)) + + (log "BUY" addr *caller* current-price) + addr))) + +(defn sell + ^{:callable true + :doc {:description "Sell an account back to the recycling market. + Caller must control the account. Receives payment at sell price."}} + [addr] + (let [addr (address addr) + buy-price (-price-for-available) + sell-p (-sell-price buy-price)] + ;; Verify caller controls the account + (or (= *caller* addr) + (fail :TRUST "You can only sell accounts you control")) + ;; Verify contract has funds to buy back + (or (>= (balance *address*) sell-p) (fail :FUNDS "Market insufficient funds")) + + ;; Clear the account + (-clear-account! addr) + + ;; Transfer payment to seller + (transfer *caller* sell-p) + + ;; Update inventory + (set! *inventory* (assoc *inventory* addr {:status :available :returned-by *caller*})) + (set! *available-count* (inc *available-count*)) + + (log "SELL" addr *caller* sell-p) + sell-p)) + +(defn withdraw-funds + ^{:callable true + :doc {:description "Withdraw accumulated funds. Only callable by contract controller."}} + [amount] + (or (trust/trusted? *address* *caller* :withdraw) (fail :TRUST "Not authorized")) + (let [amount (int amount)] + (or (> amount 0) (fail :ARGUMENT "Amount must be positive")) + (or (>= (balance *address*) amount) (fail :FUNDS "Insufficient contract balance")) + (transfer *caller* amount) + amount)) + +;;;;;;;;;; Initialization + +;; Register in registry +(call *registry* + (register {:description ["Account recycling market with bonding curve pricing." + "Allows accounts to be bought and sold with automatic price discovery."] + :name "Account Recycler" + :type :actor})) diff --git a/convex-core/src/main/java/convex/core/cvm/Symbols.java b/convex-core/src/main/java/convex/core/cvm/Symbols.java index fe0a18b422..e551cc379f 100644 --- a/convex-core/src/main/java/convex/core/cvm/Symbols.java +++ b/convex-core/src/main/java/convex/core/cvm/Symbols.java @@ -136,6 +136,7 @@ public class Symbols { public static final Symbol DOUBLE = intern("double"); + public static final Symbol BIGINT = intern("bigint"); public static final Symbol BOOLEAN = intern("boolean"); public static final Symbol BOOLEAN_Q = intern("boolean?"); @@ -249,6 +250,7 @@ public class Symbols { public static final Symbol ADDRESS_Q = intern("address?"); public static final Symbol LONG_Q = intern("long?"); public static final Symbol DOUBLE_Q = intern("double?"); + public static final Symbol BIGINT_Q = intern("bigint?"); public static final Symbol STR_Q = intern("str?"); public static final Symbol NUMBER_Q = intern("number?"); public static final Symbol HASH_Q = intern("hash?"); diff --git a/convex-core/src/main/java/convex/core/lang/Core.java b/convex-core/src/main/java/convex/core/lang/Core.java index 5244803f83..b41019bfe7 100644 --- a/convex-core/src/main/java/convex/core/lang/Core.java +++ b/convex-core/src/main/java/convex/core/lang/Core.java @@ -54,6 +54,7 @@ import convex.core.data.Symbol; import convex.core.data.Vectors; import convex.core.data.prim.AInteger; +import convex.core.data.prim.CVMBigInteger; import convex.core.data.prim.ANumeric; import convex.core.data.prim.APrimitive; import convex.core.data.prim.CVMBool; @@ -1757,7 +1758,8 @@ public Context invoke(Context context, ACell[] args) { if (result == null) return context.withCastError(0,args, Types.LONG); result=result.inc(); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1772,7 +1774,8 @@ public Context invoke(Context context, ACell[] args) { if (result == null) return context.withCastError(0,args, Types.LONG); result=result.dec(); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1825,7 +1828,8 @@ public Context invoke(Context context, ACell[] args) { return context.withCastError(0, args,Types.LONG); } - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1841,11 +1845,32 @@ public Context invoke(Context context, ACell[] args) { if (a instanceof ANumeric) return context.withArgumentError("Out of range"); return context.withCastError(0, args,Types.INTEGER); } - // TODO: bigint construction cost? - return context.withResult(Juice.ARITHMETIC, result); + // BigInteger construction cost proportional to byte length + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); + + public static final CoreFn BIGINT = reg(new CoreFn<>(Symbols.BIGINT,259) { + + @Override + public Context invoke(Context context, ACell[] args) { + if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); + + ACell a = args[0]; + AInteger result = RT.castInteger(a); + if (result == null) { + if (a instanceof ANumeric) return context.withArgumentError("Out of range"); + return context.withCastError(0, args, Types.INTEGER); + } + // Cost proportional to byte length for BigInteger + long cost = Juice.costNumeric(result); + return context.withResult(cost > 0 ? cost : Juice.ARITHMETIC, result); + } + }); + + public static final CoreFn DOUBLE = reg(new CoreFn<>(Symbols.DOUBLE,139) { @Override @@ -1856,7 +1881,8 @@ public Context invoke(Context context, ACell[] args) { CVMDouble result = RT.castDouble(a); if (result == null) return context.withCastError(0, args,Types.DOUBLE); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1884,7 +1910,8 @@ public Context invoke(Context context, ACell[] args) { if (result == null) return context.withArgumentError("Invalid code point: "+cp); } - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1897,7 +1924,8 @@ public Context invoke(Context context, ACell[] args) { ACell a = args[0]; CVMLong result = RT.castByte(a); if (result == null) return context.withCastError(0,args, Types.LONG); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1915,7 +1943,8 @@ public Context invoke(Context context, ACell[] args) { ANumeric result = RT.plus(args); if (result==null) return context.withError(ErrorMessages.INVALID_NUMERIC); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1933,7 +1962,8 @@ public Context invoke(Context context, ACell[] args) { } ANumeric result = RT.minus(args); if (result==null) return context.withError(ErrorMessages.INVALID_NUMERIC); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1951,7 +1981,8 @@ public Context invoke(Context context, ACell[] args) { ANumeric result = RT.multiply(args); if (result == null) return context.withError(ErrorMessages.INVALID_NUMERIC); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1963,7 +1994,8 @@ public Context invoke(Context context, ACell[] args) { CVMDouble result = RT.divide(args); if (result == null) return context.withCastError(RT.findNonNumeric(args),args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1974,7 +2006,8 @@ public Context invoke(Context context, ACell[] args) { if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); CVMDouble result = RT.floor(args[0]); if (result == null) return context.withCastError(0,args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1986,7 +2019,8 @@ public Context invoke(Context context, ACell[] args) { if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); CVMDouble result = RT.ceil(args[0]); if (result == null) return context.withCastError(0,args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -1998,7 +2032,8 @@ public Context invoke(Context context, ACell[] args) { if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); CVMDouble result = RT.sqrt(args[0]); if (result == null) return context.withCastError(0,args, Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2016,7 +2051,8 @@ public Context invoke(Context context, ACell[] args) { } return context.withCastError(badVal,args, Types.NUMBER); } - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2027,7 +2063,8 @@ public Context invoke(Context context, ACell[] args) { if (args.length != 1) return context.withArityError(exactArityMessage(1, args.length)); ACell result = RT.signum(args[0]); if (result == null) return context.withCastError(args[0], Types.NUMBER); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2044,7 +2081,8 @@ public Context invoke(Context context, ACell[] args) { AInteger result=la.mod(lb); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2061,7 +2099,8 @@ public Context invoke(Context context, ACell[] args) { AInteger result=la.div(lb); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2078,7 +2117,8 @@ public Context invoke(Context context, ACell[] args) { AInteger result=la.rem(lb); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2095,7 +2135,8 @@ public Context invoke(Context context, ACell[] args) { AInteger result=la.quot(lb); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2127,7 +2168,8 @@ public Context invoke(Context context, ACell[] args) { CVMDouble result = RT.pow(args); if (result==null) return context.withCastError(Types.DOUBLE); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2140,7 +2182,8 @@ public Context invoke(Context context, ACell[] args) { CVMDouble result = RT.exp(args[0]); if (result==null) return context.withCastError(0,Types.DOUBLE); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2170,7 +2213,8 @@ public Context invoke(Context context, ACell[] args) { // return context.withResult(juice, result); // } else { // result=CVMDouble.create(Math.pow(base.doubleValue(), power.doubleValue())); -// return context.withResult(Juice.ARITHMETIC, result); +// long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); // } // } // }); @@ -2200,7 +2244,8 @@ public Context invoke(Context context, ACell[] args) { CVMLong result=CVMLong.create(a.longValue()&b.longValue()); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2218,7 +2263,8 @@ public Context invoke(Context context, ACell[] args) { CVMLong result=CVMLong.create(a.longValue()^b.longValue()); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2236,7 +2282,8 @@ public Context invoke(Context context, ACell[] args) { CVMLong result=CVMLong.create(a.longValue()|b.longValue()); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2251,7 +2298,8 @@ public Context invoke(Context context, ACell[] args) { CVMLong result=CVMLong.create(~a.longValue()); - return context.withResult(Juice.ARITHMETIC, result); + long cost = Juice.costNumeric(result); + return context.withResult(cost>0?cost:Juice.ARITHMETIC, result); } }); @@ -2854,6 +2902,12 @@ public boolean test(ACell val) { return val instanceof CVMDouble; } }); + public static final CoreFn BIGINT_Q = reg(new CorePred(Symbols.BIGINT_Q,260) { + @Override + public boolean test(ACell val) { + return val instanceof CVMBigInteger; + } + }); public static final CoreFn STR_Q = reg(new CorePred(Symbols.STR_Q,254) { @Override diff --git a/convex-core/src/test/cvx/test/convex/account-recycler.cvx b/convex-core/src/test/cvx/test/convex/account-recycler.cvx new file mode 100644 index 0000000000..3f60f44116 --- /dev/null +++ b/convex-core/src/test/cvx/test/convex/account-recycler.cvx @@ -0,0 +1,91 @@ +'convex.test.account-recycler + +;; Tests for Account Recycler contract +;; Run with: (load "convex/test/account-recycler.cvx") + +(import convex.trust :as trust) + +;;;;;;;;;; Setup + +(def recycler + (deploy '(do + (import convex.account-recycler :as ar) + ;; Fund the recycler + (accept 100000)))) + +;;;;;;;;;; Test: Price discovery + +(assert (= (call recycler (price)) (call recycler (price))) + "Price should be deterministic") + +(assert (> (call recycler (price)) 0) + "Price should be positive") + +;;;;;;;;;; Test: Deposit and buy cycle + +;; Create test accounts +(def test-acct-1 (deploy '(def x 42))) +(def test-acct-2 (deploy '(def y 99))) + +;; Deposit first account +(call test-acct-1 (set-controller *address*)) +(call recycler (deposit test-acct-1)) + +(assert (= (call recycler (available-count)) 1) + "Should have 1 available account after deposit") + +;; Buy the account +(let [p (call recycler (price))] + (call recycler (buy)) + ;; After buying, we should have the account as controller + (assert (= (call recycler (available-count)) 0) + "Should have 0 available after buy")) + +;;;;;;;;;; Test: Sell back + +;; Sell the account back +(call recycler (sell test-acct-1)) + +(assert (= (call recycler (available-count)) 1) + "Should have 1 available after sell") + +;;;;;;;;;; Test: Bonding curve + +;; Deposit more accounts to test price changes +(call test-acct-2 (set-controller *address*)) +(call recycler (deposit test-acct-2)) + +(let [p1 (call recycler (price))] + ;; Price should change with more inventory + (assert (> p1 0) "Price should remain positive")) + +;;;;;;;;;; Test: Security + +;; Can't deposit account you don't control +(def other-acct (deploy '(def z 7))) +(should-fail + (call recycler (deposit other-acct)) + "Should fail to deposit uncontrolled account") + +;; Can't buy when no accounts available +(should-fail + (do + ;; Buy all available + (while (> (call recycler (available-count)) 0) + (call recycler (buy))) + ;; Try to buy when empty + (call recycler (buy))) + "Should fail when no accounts available") + +;;;;;;;;;; Test: Account clearing + +;; When an account is deposited, it should be cleared +(def clean-acct (deploy '(do (def secret 42) (def data [1 2 3])))) +(call clean-acct (set-controller *address*)) +(call recycler (deposit clean-acct)) + +;; After deposit, the account's environment should be cleared +;; (The defs should be undef'd) + +;;;;;;;;;; Summary +(println "All account-recycler tests passed!")