From e321b6c2577019384b2d972ef06067e19425e086 Mon Sep 17 00:00:00 2001 From: Brecht Devos Date: Tue, 8 Jun 2021 17:00:24 +0200 Subject: [PATCH] [Protocol 3.8] AMM v2 - Mirage (#2432) * [protocol 3] Initial commit amm v2 * [protocol 3.8] Amm v2 improvements * Small improvements * Small simplification * Update AmmData.sol * Amm v2 - AssetManager fix (#2445) * [AMM] Fix pools with active asset manager * Feedback * Amm v2 - Exit mode and pool reuse (#2450) * AMM v2 - minor improvement over brecht's code (#2458) Co-authored-by: Daniel Wang * Amm v2 improve2 (#2462) * Update AmplifiedAmmController.sol * Update AmplifiedAmmController.sol * AMM v2: minor improvements (#2466) Co-authored-by: Daniel Wang --- packages/loopring_v3.js/src/exchange_v3.ts | 2 +- .../spot_trade_processor.ts | 18 + .../circuit/Circuits/BaseTransactionCircuit.h | 6 + .../circuit/Circuits/SpotTradeCircuit.h | 100 ++++- .../circuit/Circuits/UniversalCircuit.h | 6 +- .../circuit/Gadgets/AccountGadgets.h | 2 +- .../circuit/Gadgets/MatchingGadgets.h | 146 +++--- .../loopring_v3/circuit/Gadgets/MathGadgets.h | 424 ------------------ .../circuit/test/MatchingTests.cpp | 123 ++--- .../loopring_v3/circuit/test/MathTests.cpp | 400 ----------------- packages/loopring_v3/circuit/test/TestUtils.h | 39 -- .../contracts/amm/AmplifiedAmmController.sol | 140 ++++++ .../contracts/amm/IAmmController.sol | 34 ++ .../contracts/amm/IAssetManager.sol | 8 + .../contracts/amm/LoopringAmmPool.sol | 94 +++- .../amm/libamm/AmmAssetManagement.sol | 52 +++ .../contracts/amm/libamm/AmmData.sol | 43 +- .../amm/libamm/AmmDepositProcess.sol | 74 +++ .../contracts/amm/libamm/AmmExitProcess.sol | 30 +- .../contracts/amm/libamm/AmmJoinProcess.sol | 23 +- .../contracts/amm/libamm/AmmPoolToken.sol | 2 +- .../contracts/amm/libamm/AmmSignature.sol | 25 ++ .../contracts/amm/libamm/AmmStatus.sol | 97 ++-- .../amm/libamm/AmmTransactionReceiver.sol | 65 ++- .../contracts/amm/libamm/AmmUpdateProcess.sol | 29 +- .../amm/libamm/AmmVirtualBalanceProcess.sol | 34 ++ .../amm/libamm/AmmWithdrawProcess.sol | 82 ++++ .../aux/access/DelayedTransaction.sol | 4 +- .../aux/access/LoopringIOExchangeOwner.sol | 14 +- .../aux/access/SelectorBasedAccessManager.sol | 2 +- .../contracts/aux/bridge/BatchDepositor.sol | 8 +- .../contracts/core/iface/ExchangeData.sol | 2 +- .../loopring_v3/contracts/lib/LPToken.sol | 8 +- .../loopring_v3/contracts/test/LRCToken.sol | 2 +- .../contracts/test/LoopringAmmPoolCopy.sol | 7 +- .../contracts/test/TestAssetManager.sol | 84 ++++ .../test/TestMigrationBridgeConnector.sol | 2 +- .../test/TestSwappperBridgeConnector.sol | 2 +- .../contracts/thirdparty/MockContract.sol | 28 +- .../loopring_v3/migrations/7_deploy_amm.js | 29 +- packages/loopring_v3/operator/state.py | 39 +- packages/loopring_v3/package.json | 2 +- packages/loopring_v3/test/ammUtils.ts | 311 ++++++++++++- packages/loopring_v3/test/simulator.ts | 18 + packages/loopring_v3/test/testAMMPool.ts | 299 +++++++++++- packages/loopring_v3/test/testExchangeAMM.ts | 88 +++- packages/loopring_v3/test/testExchangeUtil.ts | 19 +- 47 files changed, 1809 insertions(+), 1257 deletions(-) create mode 100644 packages/loopring_v3/contracts/amm/AmplifiedAmmController.sol create mode 100644 packages/loopring_v3/contracts/amm/IAmmController.sol create mode 100644 packages/loopring_v3/contracts/amm/IAssetManager.sol create mode 100644 packages/loopring_v3/contracts/amm/libamm/AmmAssetManagement.sol create mode 100644 packages/loopring_v3/contracts/amm/libamm/AmmDepositProcess.sol create mode 100644 packages/loopring_v3/contracts/amm/libamm/AmmSignature.sol create mode 100644 packages/loopring_v3/contracts/amm/libamm/AmmVirtualBalanceProcess.sol create mode 100644 packages/loopring_v3/contracts/amm/libamm/AmmWithdrawProcess.sol create mode 100644 packages/loopring_v3/contracts/test/TestAssetManager.sol diff --git a/packages/loopring_v3.js/src/exchange_v3.ts b/packages/loopring_v3.js/src/exchange_v3.ts index e0aee9851..2ad32dee4 100644 --- a/packages/loopring_v3.js/src/exchange_v3.ts +++ b/packages/loopring_v3.js/src/exchange_v3.ts @@ -626,7 +626,7 @@ export class ExchangeV3 { // Get the block data from the transaction data //const submitBlocksFunctionSignature = "0x8dadd3af"; // submitBlocks - const submitBlocksFunctionSignature = "0xc39ce618"; // submitBlocksWithCallbacks + const submitBlocksFunctionSignature = "0xfbb404ab"; // submitBlocksWithCallbacks const transaction = await this.web3.eth.getTransaction( event.transactionHash diff --git a/packages/loopring_v3.js/src/request_processors/spot_trade_processor.ts b/packages/loopring_v3.js/src/request_processors/spot_trade_processor.ts index 77dcf7a55..61a09951c 100644 --- a/packages/loopring_v3.js/src/request_processors/spot_trade_processor.ts +++ b/packages/loopring_v3.js/src/request_processors/spot_trade_processor.ts @@ -100,6 +100,15 @@ export class SpotTradeProcessor { .balance.iadd(s.fillBA) .isub(s.feeA); + // virtual balances + if ( + accountA.getBalance(tokenA).weightAMM.gt(new BN(0)) && + accountA.getBalance(tokenB).weightAMM.gt(new BN(0)) + ) { + accountA.getBalance(tokenA).weightAMM.isub(s.fillSA); + accountA.getBalance(tokenB).weightAMM.iadd(s.fillBA); + } + const tradeHistoryA = accountA.getBalance(tokenA).getStorage(storageIdA); if (tradeHistoryA.storageID !== storageIdA) { tradeHistoryA.data = new BN(0); @@ -116,6 +125,15 @@ export class SpotTradeProcessor { .balance.iadd(s.fillBB) .isub(s.feeB); + // virtual balances + if ( + accountB.getBalance(tokenA).weightAMM.gt(new BN(0)) && + accountB.getBalance(tokenB).weightAMM.gt(new BN(0)) + ) { + accountB.getBalance(tokenB).weightAMM.isub(s.fillBA); + accountB.getBalance(tokenA).weightAMM.iadd(s.fillSA); + } + const tradeHistoryB = accountB.getBalance(tokenB).getStorage(storageIdB); if (tradeHistoryB.storageID !== storageIdB) { tradeHistoryB.data = new BN(0); diff --git a/packages/loopring_v3/circuit/Circuits/BaseTransactionCircuit.h b/packages/loopring_v3/circuit/Circuits/BaseTransactionCircuit.h index 8cff38745..ea3240412 100644 --- a/packages/loopring_v3/circuit/Circuits/BaseTransactionCircuit.h +++ b/packages/loopring_v3/circuit/Circuits/BaseTransactionCircuit.h @@ -180,6 +180,7 @@ enum TxVariable TXV_BALANCE_A_S_WEIGHTAMM, TXV_BALANCE_A_B_BALANCE, + TXV_BALANCE_A_B_WEIGHTAMM, TXV_ACCOUNT_A_ADDRESS, TXV_ACCOUNT_A_OWNER, @@ -194,8 +195,10 @@ enum TxVariable TXV_BALANCE_B_S_ADDRESS, TXV_BALANCE_B_S_BALANCE, + TXV_BALANCE_B_S_WEIGHTAMM, TXV_BALANCE_B_B_BALANCE, + TXV_BALANCE_B_B_WEIGHTAMM, TXV_ACCOUNT_B_ADDRESS, TXV_ACCOUNT_B_OWNER, @@ -245,6 +248,7 @@ class BaseTransactionCircuit : public GadgetT uOutputs[TXV_BALANCE_A_S_WEIGHTAMM] = state.accountA.balanceS.weightAMM; uOutputs[TXV_BALANCE_A_B_BALANCE] = state.accountA.balanceB.balance; + uOutputs[TXV_BALANCE_A_B_WEIGHTAMM] = state.accountA.balanceB.weightAMM; aOutputs[TXV_ACCOUNT_A_ADDRESS] = flatten({VariableArrayT(1, state.constants._1), VariableArrayT(NUM_BITS_ACCOUNT - 1, state.constants._0)}); @@ -260,8 +264,10 @@ class BaseTransactionCircuit : public GadgetT aOutputs[TXV_BALANCE_B_S_ADDRESS] = VariableArrayT(NUM_BITS_TOKEN, state.constants._0); uOutputs[TXV_BALANCE_B_S_BALANCE] = state.accountB.balanceS.balance; + uOutputs[TXV_BALANCE_B_S_WEIGHTAMM] = state.accountB.balanceS.weightAMM; uOutputs[TXV_BALANCE_B_B_BALANCE] = state.accountB.balanceB.balance; + uOutputs[TXV_BALANCE_B_B_WEIGHTAMM] = state.accountB.balanceB.weightAMM; aOutputs[TXV_ACCOUNT_B_ADDRESS] = flatten({VariableArrayT(1, state.constants._1), VariableArrayT(NUM_BITS_ACCOUNT - 1, state.constants._0)}); diff --git a/packages/loopring_v3/circuit/Circuits/SpotTradeCircuit.h b/packages/loopring_v3/circuit/Circuits/SpotTradeCircuit.h index 17e08aa4e..6d8c4e367 100644 --- a/packages/loopring_v3/circuit/Circuits/SpotTradeCircuit.h +++ b/packages/loopring_v3/circuit/Circuits/SpotTradeCircuit.h @@ -32,6 +32,11 @@ class SpotTradeCircuit : public BaseTransactionCircuit DynamicBalanceGadget balanceA_O; DynamicBalanceGadget balanceB_O; + DynamicBalanceGadget vbalanceS_A; + DynamicBalanceGadget vbalanceB_A; + DynamicBalanceGadget vbalanceS_B; + DynamicBalanceGadget vbalanceB_B; + // Order fills FloatGadget fillS_A; FloatGadget fillS_B; @@ -59,6 +64,16 @@ class SpotTradeCircuit : public BaseTransactionCircuit TransferGadget protocolFeeA_from_balanceAO_to_balanceAP; TransferGadget protocolFeeB_from_balanceBO_to_balanceBP; + /* Virtual Token Transfers */ + TernaryGadget vfills_S_A; + TernaryGadget vfills_B_A; + TernaryGadget vfills_S_B; + TernaryGadget vfills_B_B; + SubGadget update_vbalanceS_A; + AddGadget update_vbalanceB_A; + SubGadget update_vbalanceS_B; + AddGadget update_vbalanceB_B; + // AMM validation ValidateAMMGadget validateAMM; @@ -82,6 +97,12 @@ class SpotTradeCircuit : public BaseTransactionCircuit balanceA_O(pb, state.oper.balanceA, FMT(prefix, ".balanceA_O")), balanceB_O(pb, state.oper.balanceB, FMT(prefix, ".balanceB_O")), + // Virtual balances + vbalanceS_A(pb, state.accountA.balanceS.weightAMM, FMT(prefix, ".vbalanceS_A")), + vbalanceB_A(pb, state.accountA.balanceB.weightAMM, FMT(prefix, ".vbalanceB_A")), + vbalanceS_B(pb, state.accountB.balanceS.weightAMM, FMT(prefix, ".vbalanceS_B")), + vbalanceB_B(pb, state.accountB.balanceB.weightAMM, FMT(prefix, ".vbalanceB_B")), + // Order fills fillS_A(pb, state.constants, Float24Encoding, FMT(prefix, ".fillS_A")), fillS_B(pb, state.constants, Float24Encoding, FMT(prefix, ".fillS_B")), @@ -175,28 +196,55 @@ class SpotTradeCircuit : public BaseTransactionCircuit feeCalculatorB.getProtocolFee(), FMT(prefix, ".protocolFeeB_from_balanceBO_to_balanceBP")), + /* Virtual balance updates (for AMMs only) */ + vfills_S_A(pb, orderA.amm.packed, fillS_A.value(), state.constants._0, FMT(prefix, ".vfills_S_A")), + vfills_B_A(pb, orderA.amm.packed, fillS_B.value(), state.constants._0, FMT(prefix, ".vfills_B_A")), + vfills_S_B(pb, orderB.amm.packed, fillS_B.value(), state.constants._0, FMT(prefix, ".vfills_S_B")), + vfills_B_B(pb, orderB.amm.packed, fillS_A.value(), state.constants._0, FMT(prefix, ".vfills_B_B")), + update_vbalanceS_A( + pb, + state.accountA.balanceS.weightAMM, + vfills_S_A.result(), + NUM_BITS_AMOUNT, + FMT(prefix, ".update_vbalanceS_A")), + update_vbalanceB_A( + pb, + state.accountA.balanceB.weightAMM, + vfills_B_A.result(), + NUM_BITS_AMOUNT, + FMT(prefix, ".update_vbalanceB_A")), + update_vbalanceS_B( + pb, + state.accountB.balanceS.weightAMM, + vfills_S_B.result(), + NUM_BITS_AMOUNT, + FMT(prefix, ".update_vbalanceS_B")), + update_vbalanceB_B( + pb, + state.accountB.balanceB.weightAMM, + vfills_B_B.result(), + NUM_BITS_AMOUNT, + FMT(prefix, ".update_vbalanceB_B")), + validateAMM( pb, state.constants, + isSpotTradeTx.result(), {orderA.amm.packed, orderA.feeBips.packed, fillS_A.value(), - state.accountA.balanceS.balance, - state.accountA.balanceB.balance, - balanceS_A.balance(), - balanceB_A.balance(), state.accountA.balanceS.weightAMM, state.accountA.balanceB.weightAMM, + update_vbalanceS_A.result(), + update_vbalanceB_A.result(), state.accountA.account.feeBipsAMM}, {orderB.amm.packed, orderB.feeBips.packed, fillS_B.value(), - state.accountB.balanceS.balance, - state.accountB.balanceB.balance, - balanceS_B.balance(), - balanceB_B.balance(), state.accountB.balanceS.weightAMM, state.accountB.balanceB.weightAMM, + update_vbalanceS_B.result(), + update_vbalanceB_B.result(), state.accountB.account.feeBipsAMM}, FMT(prefix, ".validateAMM")) { @@ -210,6 +258,8 @@ class SpotTradeCircuit : public BaseTransactionCircuit setOutput(TXV_STORAGE_A_STORAGEID, orderA.storageID.packed); setOutput(TXV_BALANCE_A_S_BALANCE, balanceS_A.balance()); setOutput(TXV_BALANCE_A_B_BALANCE, balanceB_A.balance()); + setOutput(TXV_BALANCE_A_S_WEIGHTAMM, update_vbalanceS_A.result()); + setOutput(TXV_BALANCE_A_B_WEIGHTAMM, update_vbalanceB_A.result()); setArrayOutput(TXV_ACCOUNT_A_ADDRESS, orderA.accountID.bits); // Update account B @@ -218,6 +268,8 @@ class SpotTradeCircuit : public BaseTransactionCircuit setOutput(TXV_STORAGE_B_STORAGEID, orderB.storageID.packed); setOutput(TXV_BALANCE_B_S_BALANCE, balanceS_B.balance()); setOutput(TXV_BALANCE_B_B_BALANCE, balanceB_B.balance()); + setOutput(TXV_BALANCE_B_S_WEIGHTAMM, update_vbalanceS_B.result()); + setOutput(TXV_BALANCE_B_B_WEIGHTAMM, update_vbalanceB_B.result()); setArrayOutput(TXV_ACCOUNT_B_ADDRESS, orderB.accountID.bits); // Update balances of the protocol fee pool @@ -252,6 +304,12 @@ class SpotTradeCircuit : public BaseTransactionCircuit balanceA_O.generate_r1cs_witness(); balanceB_O.generate_r1cs_witness(); + // Virtual balances + vbalanceS_A.generate_r1cs_witness(); + vbalanceB_A.generate_r1cs_witness(); + vbalanceS_B.generate_r1cs_witness(); + vbalanceB_B.generate_r1cs_witness(); + // Order fills fillS_A.generate_r1cs_witness(spotTrade.fillS_A); fillS_B.generate_r1cs_witness(spotTrade.fillS_B); @@ -279,6 +337,16 @@ class SpotTradeCircuit : public BaseTransactionCircuit protocolFeeA_from_balanceAO_to_balanceAP.generate_r1cs_witness(); protocolFeeB_from_balanceBO_to_balanceBP.generate_r1cs_witness(); + /* Virtual Token Transfers */ + vfills_S_A.generate_r1cs_witness(); + vfills_B_A.generate_r1cs_witness(); + vfills_S_B.generate_r1cs_witness(); + vfills_B_B.generate_r1cs_witness(); + update_vbalanceS_A.generate_r1cs_witness(); + update_vbalanceB_A.generate_r1cs_witness(); + update_vbalanceS_B.generate_r1cs_witness(); + update_vbalanceB_B.generate_r1cs_witness(); + // AMM validation validateAMM.generate_r1cs_witness(); } @@ -299,6 +367,12 @@ class SpotTradeCircuit : public BaseTransactionCircuit balanceA_O.generate_r1cs_constraints(); balanceB_O.generate_r1cs_constraints(); + // Virtual balances + vbalanceS_A.generate_r1cs_constraints(); + vbalanceB_A.generate_r1cs_constraints(); + vbalanceS_B.generate_r1cs_constraints(); + vbalanceB_B.generate_r1cs_constraints(); + // Order fills fillS_A.generate_r1cs_constraints(); fillS_B.generate_r1cs_constraints(); @@ -326,6 +400,16 @@ class SpotTradeCircuit : public BaseTransactionCircuit protocolFeeA_from_balanceAO_to_balanceAP.generate_r1cs_constraints(); protocolFeeB_from_balanceBO_to_balanceBP.generate_r1cs_constraints(); + /* Virtual Token Transfers */ + vfills_S_A.generate_r1cs_constraints(); + vfills_B_A.generate_r1cs_constraints(); + vfills_S_B.generate_r1cs_constraints(); + vfills_B_B.generate_r1cs_constraints(); + update_vbalanceS_A.generate_r1cs_constraints(); + update_vbalanceB_A.generate_r1cs_constraints(); + update_vbalanceS_B.generate_r1cs_constraints(); + update_vbalanceB_B.generate_r1cs_constraints(); + // AMM validation validateAMM.generate_r1cs_constraints(); } diff --git a/packages/loopring_v3/circuit/Circuits/UniversalCircuit.h b/packages/loopring_v3/circuit/Circuits/UniversalCircuit.h index ffa275c28..d6427f161 100644 --- a/packages/loopring_v3/circuit/Circuits/UniversalCircuit.h +++ b/packages/loopring_v3/circuit/Circuits/UniversalCircuit.h @@ -287,7 +287,7 @@ class TransactionGadget : public GadgetT tx.getArrayOutput(TXV_BALANCE_B_S_ADDRESS), {state.accountA.balanceB.balance, state.accountA.balanceB.weightAMM, state.accountA.balanceB.storageRoot}, {tx.getOutput(TXV_BALANCE_A_B_BALANCE), - state.accountA.balanceB.weightAMM, + tx.getOutput(TXV_BALANCE_A_B_WEIGHTAMM), state.accountA.balanceB.storageRoot}, FMT(prefix, ".updateBalanceB_A")), updateAccount_A( @@ -321,7 +321,7 @@ class TransactionGadget : public GadgetT state.accountB.account.balancesRoot, tx.getArrayOutput(TXV_BALANCE_B_S_ADDRESS), {state.accountB.balanceS.balance, state.accountB.balanceS.weightAMM, state.accountB.balanceS.storageRoot}, - {tx.getOutput(TXV_BALANCE_B_S_BALANCE), state.accountB.balanceS.weightAMM, updateStorage_B.result()}, + {tx.getOutput(TXV_BALANCE_B_S_BALANCE), tx.getOutput(TXV_BALANCE_B_S_WEIGHTAMM), updateStorage_B.result()}, FMT(prefix, ".updateBalanceS_B")), updateBalanceB_B( pb, @@ -329,7 +329,7 @@ class TransactionGadget : public GadgetT tx.getArrayOutput(TXV_BALANCE_A_S_ADDRESS), {state.accountB.balanceB.balance, state.accountB.balanceB.weightAMM, state.accountB.balanceB.storageRoot}, {tx.getOutput(TXV_BALANCE_B_B_BALANCE), - state.accountB.balanceB.weightAMM, + tx.getOutput(TXV_BALANCE_B_B_WEIGHTAMM), state.accountB.balanceB.storageRoot}, FMT(prefix, ".updateBalanceB_B")), updateAccount_B( diff --git a/packages/loopring_v3/circuit/Gadgets/AccountGadgets.h b/packages/loopring_v3/circuit/Gadgets/AccountGadgets.h index dc6dd7d0a..9c579b4bc 100644 --- a/packages/loopring_v3/circuit/Gadgets/AccountGadgets.h +++ b/packages/loopring_v3/circuit/Gadgets/AccountGadgets.h @@ -301,7 +301,7 @@ class UpdateBalanceGadget : public GadgetT } }; -// Calculcates the state of a user's open position +// Calculcates the state of a user's balance class DynamicBalanceGadget : public DynamicVariableGadget { public: diff --git a/packages/loopring_v3/circuit/Gadgets/MatchingGadgets.h b/packages/loopring_v3/circuit/Gadgets/MatchingGadgets.h index d6a514e1d..20156664b 100644 --- a/packages/loopring_v3/circuit/Gadgets/MatchingGadgets.h +++ b/packages/loopring_v3/circuit/Gadgets/MatchingGadgets.h @@ -483,18 +483,15 @@ class OrderMatchingGadget : public GadgetT // const calcSpotPrice = ( // balanceIn: number, -// weightIn: number, -// balanceOut: number, -// weightOut: number) => { -// const numer = balanceIn * weightOut; -// const denom = balanceOut * weightIn; +// balanceOut: number) => { +// const numer = balanceIn; +// const denom = balanceOut; // const ratio = (numer * BASE_FIXED) / denom; // const invFeeBips = BASE_BIPS - feeBips; // return Math.floor((ratio * BASE_BIPS) / invFeeBips); // } // Result is guaranteed to fit inside NUM_BITS_AMOUNT*2 bits. -// Max ratio between weights guaranteed to be supported is 2**(96*2-14)/(10**18) // but this depends on the balances inside the pool as well. Normally it should be even much higher. class SpotPriceAMMGadget : public GadgetT { @@ -510,15 +507,13 @@ class SpotPriceAMMGadget : public GadgetT ProtoboardT &pb, const Constants &constants, const VariableT &balanceIn, - const VariableT &weightIn, const VariableT &balanceOut, - const VariableT &weightOut, const VariableT &feeBips, const std::string &prefix) : GadgetT(pb, prefix), - numer(pb, balanceIn, weightOut, FMT(prefix, ".numer")), - denom(pb, balanceOut, weightIn, FMT(prefix, ".denom")), + numer(pb, balanceIn, constants._1, FMT(prefix, ".numer")), + denom(pb, balanceOut, constants._1, FMT(prefix, ".denom")), ratio( pb, constants, @@ -576,28 +571,19 @@ class SpotPriceAMMGadget : public GadgetT // const calcOutGivenIn = ( // balanceIn: number, -// weightIn: number, // balanceOut: number, -// weightOut: number, // amountIn: number) => { -// const weightRatio = (weightIn * BASE_FIXED) / weightOut; // const fee = amountIn * feeBips / BASE_BIPS; // const y = (balanceIn * BASE_FIXED) / (balanceIn + (amountIn - fee)); -// const p = pow_approx(y, weightRatio); -// return Math.floor(balanceOut * (BASE_FIXED - p) / BASE_FIXED); +// return Math.floor(balanceOut * (BASE_FIXED - y) / BASE_FIXED); // } class CalcOutGivenInAMMGadget : public GadgetT { public: - static const unsigned int numIterations = 4; - - MulDivGadget weightRatio; - RangeCheckGadget weightRatioRangeCheck; MulDivGadget fee; UnsafeSubGadget amountInWithoutFee; AddGadget y_denom; MulDivGadget y; - PowerGadget p; SubGadget invP; MulDivGadget res; @@ -605,29 +591,12 @@ class CalcOutGivenInAMMGadget : public GadgetT ProtoboardT &pb, const Constants &constants, const VariableT &balanceIn, - const VariableT &weightIn, const VariableT &balanceOut, - const VariableT &weightOut, const VariableT &feeBips, const VariableT &amountIn, const std::string &prefix) : GadgetT(pb, prefix), - weightRatio( - pb, - constants, - weightIn, - constants.fixedBase, - weightOut, - NUM_BITS_AMOUNT, - NUM_BITS_FIXED_BASE, - NUM_BITS_AMOUNT, - FMT(prefix, ".weightRatio")), - weightRatioRangeCheck( // - pb, - weightRatio.result(), - NUM_BITS_AMOUNT, - FMT(prefix, ".weightRatioRangeCheck")), fee( pb, constants, @@ -659,17 +628,10 @@ class CalcOutGivenInAMMGadget : public GadgetT NUM_BITS_FIXED_BASE, NUM_BITS_AMOUNT, FMT(prefix, ".y")), - p( // - pb, - constants, - y.result(), - weightRatio.result(), - numIterations, - FMT(prefix, ".p")), invP( // pb, constants.fixedBase, - p.result(), + y.result(), NUM_BITS_FIXED_BASE, FMT(prefix, ".invP")), res( @@ -687,26 +649,20 @@ class CalcOutGivenInAMMGadget : public GadgetT void generate_r1cs_witness() { - weightRatio.generate_r1cs_witness(); - weightRatioRangeCheck.generate_r1cs_witness(); fee.generate_r1cs_witness(); amountInWithoutFee.generate_r1cs_witness(); y_denom.generate_r1cs_witness(); y.generate_r1cs_witness(); - p.generate_r1cs_witness(); invP.generate_r1cs_witness(); res.generate_r1cs_witness(); } void generate_r1cs_constraints() { - weightRatio.generate_r1cs_constraints(); - weightRatioRangeCheck.generate_r1cs_constraints(); fee.generate_r1cs_constraints(); amountInWithoutFee.generate_r1cs_constraints(); y_denom.generate_r1cs_constraints(); y.generate_r1cs_constraints(); - p.generate_r1cs_constraints(); invP.generate_r1cs_constraints(); res.generate_r1cs_constraints(); } @@ -726,8 +682,6 @@ struct OrderMatchingData const VariableT &balanceBeforeB; const VariableT &balanceAfterS; const VariableT &balanceAfterB; - const VariableT &weightS; - const VariableT &weightB; const VariableT &ammFeeBips; }; @@ -735,19 +689,25 @@ struct OrderMatchingData class RequireAMMFillsGadget : public GadgetT { public: + // If any of the balances are non-zero, amm needs to be enabled + IsNonZero inBalanceNonZero; + IsNonZero outBalanceNonZero; + AndGadget ammConditionS; + AndGadget ammConditionB; + IfThenRequireGadget requireAmmSetS; + IfThenRequireGadget requireAmmSetB; + // Use dummy data if this isn't an AMM order TernaryGadget inBalanceBefore; TernaryGadget inBalanceAfter; - TernaryGadget inWeight; TernaryGadget outBalanceBefore; TernaryGadget outBalanceAfter; - TernaryGadget outWeight; TernaryGadget ammFill; // Verify general assumptions AMM orders IfThenRequireEqualGadget requireOrderFeeBipsZero; - RequireNotZeroGadget requireInWeightNotZero; - RequireNotZeroGadget requireOutWeightNotZero; + RequireNotZeroGadget requireInBalanceNotZero; + RequireNotZeroGadget requireOutBalanceNotZero; // Calculate AMM minimum rate CalcOutGivenInAMMGadget ammMaximumFillS; @@ -763,11 +723,20 @@ class RequireAMMFillsGadget : public GadgetT RequireAMMFillsGadget( ProtoboardT &pb, const Constants &constants, + const VariableT &isSpotTradeTx, const OrderMatchingData &data, const VariableT &fillB, const std::string &prefix) : GadgetT(pb, prefix), + // If any of the balances are non-zero, amm needs to be enabled + inBalanceNonZero(pb, data.balanceBeforeB, FMT(prefix, ".inBalanceNonZero")), + outBalanceNonZero(pb, data.balanceBeforeS, FMT(prefix, ".outBalanceNonZero")), + ammConditionS(pb, {isSpotTradeTx, inBalanceNonZero.result()}, FMT(prefix, ".ammConditionS")), + ammConditionB(pb, {isSpotTradeTx, inBalanceNonZero.result()}, FMT(prefix, ".ammConditionB")), + requireAmmSetS(pb, ammConditionS.result(), data.amm, FMT(prefix, ".requireAmmSetS")), + requireAmmSetB(pb, ammConditionB.result(), data.amm, FMT(prefix, ".requireAmmSetB")), + // Use dummy data if this isn't an AMM order inBalanceBefore( // pb, @@ -781,12 +750,6 @@ class RequireAMMFillsGadget : public GadgetT data.balanceAfterB, constants.fixedBase, FMT(prefix, ".inBalanceAfter")), - inWeight( // - pb, - data.amm, - data.weightB, - constants.fixedBase, - FMT(prefix, ".inWeight")), outBalanceBefore( // pb, data.amm, @@ -799,12 +762,6 @@ class RequireAMMFillsGadget : public GadgetT data.balanceAfterS, constants.fixedBase, FMT(prefix, ".outBalanceAfter")), - outWeight( // - pb, - data.amm, - data.weightS, - constants.fixedBase, - FMT(prefix, ".outWeight")), ammFill( // pb, data.amm, @@ -819,23 +776,21 @@ class RequireAMMFillsGadget : public GadgetT data.orderFeeBips, constants._0, FMT(prefix, ".requireOrderFeeBipsZero")), - requireInWeightNotZero( // + requireInBalanceNotZero( // pb, - inWeight.result(), - FMT(prefix, ".requireInWeightNotZero")), - requireOutWeightNotZero( // + inBalanceBefore.result(), + FMT(prefix, ".requireInBalanceNotZero")), + requireOutBalanceNotZero( // pb, - outWeight.result(), - FMT(prefix, ".requireOutWeightNotZero")), + outBalanceBefore.result(), + FMT(prefix, ".requireOutBalanceNotZero")), // Calculate AMM minimum rate ammMaximumFillS( pb, constants, inBalanceBefore.result(), - inWeight.result(), outBalanceBefore.result(), - outWeight.result(), data.ammFeeBips, ammFill.result(), FMT(prefix, ".ammMaximumFillS")), @@ -856,18 +811,14 @@ class RequireAMMFillsGadget : public GadgetT pb, constants, inBalanceBefore.result(), - inWeight.result(), outBalanceBefore.result(), - outWeight.result(), data.ammFeeBips, FMT(prefix, ".priceBefore")), priceAfter( pb, constants, inBalanceAfter.result(), - inWeight.result(), outBalanceAfter.result(), - outWeight.result(), data.ammFeeBips, FMT(prefix, ".priceBefore")), priceBefore_leq_priceAfter( @@ -886,19 +837,25 @@ class RequireAMMFillsGadget : public GadgetT void generate_r1cs_witness() { + // If any of the balances are non-zero, amm needs to be enabled + inBalanceNonZero.generate_r1cs_witness(); + outBalanceNonZero.generate_r1cs_witness(); + ammConditionS.generate_r1cs_witness(); + ammConditionB.generate_r1cs_witness(); + requireAmmSetS.generate_r1cs_witness(); + requireAmmSetB.generate_r1cs_witness(); + // Use dummy data if this isn't an AMM order inBalanceBefore.generate_r1cs_witness(); inBalanceAfter.generate_r1cs_witness(); - inWeight.generate_r1cs_witness(); outBalanceBefore.generate_r1cs_witness(); outBalanceAfter.generate_r1cs_witness(); - outWeight.generate_r1cs_witness(); ammFill.generate_r1cs_witness(); // Verify general assumptions AMM orders requireOrderFeeBipsZero.generate_r1cs_witness(); - requireInWeightNotZero.generate_r1cs_witness(); - requireOutWeightNotZero.generate_r1cs_witness(); + requireInBalanceNotZero.generate_r1cs_witness(); + requireOutBalanceNotZero.generate_r1cs_witness(); // Calculate AMM minimum rate ammMaximumFillS.generate_r1cs_witness(); @@ -914,19 +871,25 @@ class RequireAMMFillsGadget : public GadgetT void generate_r1cs_constraints() { + // If any of the balances are non-zero, amm needs to be enabled + inBalanceNonZero.generate_r1cs_constraints(); + outBalanceNonZero.generate_r1cs_constraints(); + ammConditionS.generate_r1cs_constraints(); + ammConditionB.generate_r1cs_constraints(); + requireAmmSetS.generate_r1cs_constraints(); + requireAmmSetB.generate_r1cs_constraints(); + // Use dummy data if this isn't an AMM order inBalanceBefore.generate_r1cs_constraints(); inBalanceAfter.generate_r1cs_constraints(); - inWeight.generate_r1cs_constraints(); outBalanceBefore.generate_r1cs_constraints(); outBalanceAfter.generate_r1cs_constraints(); - outWeight.generate_r1cs_constraints(); ammFill.generate_r1cs_constraints(); // Verify general assumptions AMM orders requireOrderFeeBipsZero.generate_r1cs_constraints(); - requireInWeightNotZero.generate_r1cs_constraints(); - requireOutWeightNotZero.generate_r1cs_constraints(); + requireInBalanceNotZero.generate_r1cs_constraints(); + requireOutBalanceNotZero.generate_r1cs_constraints(); // Calculate AMM minimum rate ammMaximumFillS.generate_r1cs_constraints(); @@ -951,14 +914,15 @@ class ValidateAMMGadget : public GadgetT ValidateAMMGadget( ProtoboardT &pb, const Constants &constants, + const VariableT &isSpotTradeTx, const OrderMatchingData &dataA, const OrderMatchingData &dataB, const std::string &prefix) : GadgetT(pb, prefix), // Check if the fills are valid for the orders - requireFillsA(pb, constants, dataA, dataB.fillS, FMT(prefix, ".requireFillsA")), - requireFillsB(pb, constants, dataB, dataA.fillS, FMT(prefix, ".requireFillsB")) + requireFillsA(pb, constants, isSpotTradeTx, dataA, dataB.fillS, FMT(prefix, ".requireFillsA")), + requireFillsB(pb, constants, isSpotTradeTx, dataB, dataA.fillS, FMT(prefix, ".requireFillsB")) { } diff --git a/packages/loopring_v3/circuit/Gadgets/MathGadgets.h b/packages/loopring_v3/circuit/Gadgets/MathGadgets.h index 2efbb6ecf..990e352c6 100644 --- a/packages/loopring_v3/circuit/Gadgets/MathGadgets.h +++ b/packages/loopring_v3/circuit/Gadgets/MathGadgets.h @@ -1943,430 +1943,6 @@ class OwnerValidGadget : public GadgetT } }; -// Signed variable: -// positive: sign == 1 -// negative: sign == 0 -// Zero can be either positive or negative -struct SignedVariableT -{ - public: - VariableT sign; - VariableT value; - - SignedVariableT() - { - } - - SignedVariableT( // - ProtoboardT &pb, - const std::string &prefix) - : sign(make_variable(pb, FMT(prefix, ".sign"))), value(make_variable(pb, FMT(prefix, ".value"))) - { - } - - SignedVariableT( // - const VariableT &_sign, - const VariableT &_value) - : sign(_sign), value(_value) - { - } -}; - -// sA + sB = sSum with abs(A), abs(B) and abs(sum) < 2^n -class SignedAddGadget : public GadgetT -{ - public: - SignedVariableT _A; - SignedVariableT _B; - - UnsafeAddGadget a_add_b; - UnsafeSubGadget b_sub_a; - UnsafeSubGadget a_sub_b; - - LeqGadget a_leq_b; - - EqualGadget signsEqual; - TernaryGadget temp; - TernaryGadget value; - - AndGadget signB_and_a_leq_b; - AndGadget signA_and_not_a_leq_b; - OrGadget sign; - EqualGadget isZero; - TernaryGadget normalizedSign; - - RangeCheckGadget rangeCheck; - - SignedAddGadget( - ProtoboardT &pb, - const Constants &constants, - const SignedVariableT &A, - const SignedVariableT &B, - unsigned int n, - const std::string &prefix) - : GadgetT(pb, prefix), - - _A(A), - _B(B), - - a_add_b(pb, A.value, B.value, FMT(prefix, ".a_add_b")), - b_sub_a(pb, B.value, A.value, FMT(prefix, ".b_sub_a")), - a_sub_b(pb, A.value, B.value, FMT(prefix, ".a_sub_b")), - - a_leq_b(pb, A.value, B.value, n, FMT(prefix, ".a_leq_b")), - - signsEqual(pb, A.sign, B.sign, FMT(prefix, ".signsEqual")), - temp(pb, a_leq_b.lt(), b_sub_a.result(), a_sub_b.result(), FMT(prefix, ".temp")), - value(pb, signsEqual.result(), a_add_b.result(), temp.result(), FMT(prefix, ".value")), - - signB_and_a_leq_b(pb, {B.sign, a_leq_b.leq()}, FMT(prefix, ".signB_and_a_leq_b")), - signA_and_not_a_leq_b(pb, {A.sign, a_leq_b.gt()}, FMT(prefix, ".signA_and_not_a_leq_b")), - sign(pb, {signB_and_a_leq_b.result(), signA_and_not_a_leq_b.result()}, FMT(prefix, ".sign")), - isZero(pb, value.result(), constants._0, FMT(prefix, ".isZero")), - normalizedSign(pb, isZero.result(), constants._0, sign.result(), FMT(prefix, ".sign")), - - rangeCheck(pb, value.result(), n, FMT(prefix, ".rangeCheck")) - { - assert(n + 1 <= NUM_BITS_FIELD_CAPACITY); - } - - void generate_r1cs_witness() - { - a_add_b.generate_r1cs_witness(); - b_sub_a.generate_r1cs_witness(); - a_sub_b.generate_r1cs_witness(); - - a_leq_b.generate_r1cs_witness(); - - signsEqual.generate_r1cs_witness(); - temp.generate_r1cs_witness(); - value.generate_r1cs_witness(); - - signB_and_a_leq_b.generate_r1cs_witness(); - signA_and_not_a_leq_b.generate_r1cs_witness(); - sign.generate_r1cs_witness(); - isZero.generate_r1cs_witness(); - normalizedSign.generate_r1cs_witness(); - - rangeCheck.generate_r1cs_witness(); - } - - void generate_r1cs_constraints() - { - a_add_b.generate_r1cs_constraints(); - b_sub_a.generate_r1cs_constraints(); - a_sub_b.generate_r1cs_constraints(); - - a_leq_b.generate_r1cs_constraints(); - - signsEqual.generate_r1cs_constraints(); - temp.generate_r1cs_constraints(); - value.generate_r1cs_constraints(); - - signB_and_a_leq_b.generate_r1cs_constraints(); - signA_and_not_a_leq_b.generate_r1cs_constraints(); - sign.generate_r1cs_constraints(); - isZero.generate_r1cs_constraints(); - normalizedSign.generate_r1cs_constraints(); - - rangeCheck.generate_r1cs_constraints(); - } - - const SignedVariableT result() const - { - return SignedVariableT(normalizedSign.result(), value.result()); - } -}; - -// sA + (-sB) = sSum with abs(A), abs(B) and abs(sum) < 2^n -class SignedSubGadget : public GadgetT -{ - public: - NotGadget notSignB; - SignedAddGadget signedAddGadget; - - SignedSubGadget( - ProtoboardT &pb, - const Constants &constants, - const SignedVariableT &A, - const SignedVariableT &B, - unsigned int n, - const std::string &prefix) - : GadgetT(pb, prefix), - - notSignB(pb, B.sign, FMT(prefix, ".notSignB")), - signedAddGadget( - pb, - constants, - A, - SignedVariableT(notSignB.result(), B.value), - n, - FMT(prefix, ".signedAddGadget")) - { - } - - void generate_r1cs_witness() - { - notSignB.generate_r1cs_witness(); - signedAddGadget.generate_r1cs_witness(); - } - - void generate_r1cs_constraints() - { - notSignB.generate_r1cs_constraints(); - signedAddGadget.generate_r1cs_constraints(); - } - - const SignedVariableT result() const - { - return signedAddGadget.result(); - } -}; - -// sA * sB / C -// Always rounds towards zero, even for negative values. -class SignedMulDivGadget : public GadgetT -{ - public: - MulDivGadget res; - EqualGadget sign; - - EqualGadget isZero; - TernaryGadget normalizedSign; - - SignedMulDivGadget( - ProtoboardT &pb, - const Constants &constants, - const SignedVariableT &_value, - const SignedVariableT &_numerator, - const VariableT &_denominator, - unsigned int numBitsValue, - unsigned int numBitsNumerator, - unsigned int numBitsDenominator, - const std::string &prefix) - : GadgetT(pb, prefix), - - res( - pb, - constants, - _value.value, - _numerator.value, - _denominator, - numBitsValue, - numBitsNumerator, - numBitsDenominator, - FMT(prefix, ".res")), - sign(pb, _value.sign, _numerator.sign, FMT(prefix, ".sign")), - - isZero(pb, res.result(), constants._0, FMT(prefix, ".isZero")), - normalizedSign(pb, isZero.result(), constants._0, sign.result(), FMT(prefix, ".sign")) - { - } - - void generate_r1cs_witness() - { - res.generate_r1cs_witness(); - sign.generate_r1cs_witness(); - - isZero.generate_r1cs_witness(); - normalizedSign.generate_r1cs_witness(); - } - - void generate_r1cs_constraints() - { - res.generate_r1cs_constraints(); - sign.generate_r1cs_constraints(); - - isZero.generate_r1cs_constraints(); - normalizedSign.generate_r1cs_constraints(); - } - - const SignedVariableT result() const - { - return SignedVariableT(normalizedSign.result(), res.result()); - } -}; - -// Calculates [0, 1]**[0, inf) using an approximation. The closer the base is to 1, the higher the accuracy. -// The result is enforced to be containable in NUM_BITS_AMOUNT bits. -// The higher the number of iterations, the higher the accuracy (and the greater the cost). -/* - const x = (_x - BASE_FIXED); - const bn = [BASE_FIXED, BASE_FIXED]; - const cn = [BASE_FIXED, y]; - const xn = [BASE_FIXED, x]; - let sum = xn[0]*cn[0] + xn[1]*cn[1]; - for (let i = 2; i < iterations; i++) { - const v = y - bn[i-1]; - bn.push(bn[i-1] + BASE_FIXED); - xn.push(Math.floor((xn[i-1] * x) / BASE_FIXED)); - cn.push(Math.floor((cn[i-1] * v) / bn[i])); - sum += xn[i]*cn[i]; - } - return Math.floor(sum / BASE_FIXED); -*/ -class PowerGadget : public GadgetT -{ - public: - UnsafeMulGadget sum0; - - SubGadget x1; - UnsafeMulGadget t1; - SignedAddGadget sum1; - - std::vector bn; - std::vector vn; - std::vector xn; - std::vector cn; - std::vector tn; - std::vector sum; - std::vector cnRangeCheck; - - std::unique_ptr res; - std::unique_ptr resRangeCheck; - std::unique_ptr requirePositive; - - PowerGadget( - ProtoboardT &pb, - const Constants &constants, - const VariableT &x, - const VariableT &y, - const unsigned int iterations, - const std::string &prefix) - : GadgetT(pb, prefix), - - sum0(pb, constants.fixedBase, constants.fixedBase, FMT(prefix, ".sum0")), - - x1(pb, constants.fixedBase, x, NUM_BITS_FIXED_BASE, FMT(prefix, ".x1")), - t1(pb, x1.result(), y, FMT(prefix, ".t1")), - sum1( - pb, - constants, - SignedVariableT(constants._1, sum0.result()), - SignedVariableT(constants._0, t1.result()), - NUM_BITS_AMOUNT * 2, - FMT(prefix, ".sum1")) - { - assert(iterations >= 3); - - for (unsigned int i = 2; i < iterations; i++) - { - bn.emplace_back(pb, constants.fixedBase, constants.values[i], FMT(prefix, ".bn")); - vn.emplace_back( - pb, - constants, - SignedVariableT(constants._1, y), - i > 2 ? SignedVariableT(constants._1, bn[i - 2 - 1].result()) - : SignedVariableT(constants._1, constants.fixedBase), - NUM_BITS_AMOUNT, - FMT(prefix, ".vn")); - xn.emplace_back( - pb, - constants, - xn.size() > 0 ? xn.back().result() : x1.result(), - x1.result(), - constants.fixedBase, - NUM_BITS_FIXED_BASE, - NUM_BITS_FIXED_BASE, - NUM_BITS_FIXED_BASE, - FMT(prefix, ".xn")); - cn.emplace_back( - pb, - constants, - (i > 2) ? cn.back().result() : SignedVariableT(constants._1, y), - vn.back().result(), - bn.back().result(), - NUM_BITS_AMOUNT, - NUM_BITS_AMOUNT, - NUM_BITS_AMOUNT, - FMT(prefix, ".cn")); - tn.emplace_back( - pb, - constants, - SignedVariableT(constants.values[(i + 1) % 2], xn.back().result()), - cn.back().result(), - constants._1, - NUM_BITS_FIXED_BASE, - NUM_BITS_AMOUNT, - 1, - FMT(prefix, ".t2")); - sum.emplace_back( - pb, - constants, - sum.size() > 0 ? sum.back().result() : sum1.result(), - tn.back().result(), - NUM_BITS_AMOUNT * 2, - FMT(prefix, ".sum")); - cnRangeCheck.emplace_back(pb, cn.back().result().value, NUM_BITS_AMOUNT, FMT(prefix, ".cnRangeCheck")); - } - - res.reset(new MulDivGadget( - pb, - constants, - sum.back().result().value, - constants._1, - constants.fixedBase, - NUM_BITS_AMOUNT * 2, - 1, - NUM_BITS_FIXED_BASE, - FMT(prefix, ".res"))); - resRangeCheck.reset(new RangeCheckGadget(pb, res->result(), NUM_BITS_AMOUNT, FMT(prefix, ".resRangeCheck"))); - requirePositive.reset( - new RequireEqualGadget(pb, sum.back().result().sign, constants._1, FMT(prefix, ".requirePositive"))); - } - - void generate_r1cs_witness() - { - sum0.generate_r1cs_witness(); - - x1.generate_r1cs_witness(); - t1.generate_r1cs_witness(); - sum1.generate_r1cs_witness(); - - for (unsigned int i = 0; i < sum.size(); i++) - { - bn[i].generate_r1cs_witness(); - vn[i].generate_r1cs_witness(); - xn[i].generate_r1cs_witness(); - cn[i].generate_r1cs_witness(); - tn[i].generate_r1cs_witness(); - sum[i].generate_r1cs_witness(); - cnRangeCheck[i].generate_r1cs_witness(); - } - res->generate_r1cs_witness(); - resRangeCheck->generate_r1cs_witness(); - requirePositive->generate_r1cs_witness(); - } - - void generate_r1cs_constraints() - { - sum0.generate_r1cs_constraints(); - - x1.generate_r1cs_constraints(); - t1.generate_r1cs_constraints(); - sum1.generate_r1cs_constraints(); - - for (unsigned int i = 0; i < sum.size(); i++) - { - bn[i].generate_r1cs_constraints(); - vn[i].generate_r1cs_constraints(); - xn[i].generate_r1cs_constraints(); - cn[i].generate_r1cs_constraints(); - tn[i].generate_r1cs_constraints(); - sum[i].generate_r1cs_constraints(); - cnRangeCheck[i].generate_r1cs_constraints(); - } - res->generate_r1cs_constraints(); - resRangeCheck->generate_r1cs_constraints(); - requirePositive->generate_r1cs_constraints(); - } - - const VariableT &result() const - { - return res->result(); - } -}; - } // namespace Loopring #endif diff --git a/packages/loopring_v3/circuit/test/MatchingTests.cpp b/packages/loopring_v3/circuit/test/MatchingTests.cpp index 5ad92f07d..2993210a2 100644 --- a/packages/loopring_v3/circuit/test/MatchingTests.cpp +++ b/packages/loopring_v3/circuit/test/MatchingTests.cpp @@ -1162,20 +1162,13 @@ struct CalcOutResult static CalcOutResult calcOutGivenIn( const BigInt &balanceIn, - const BigInt &weightIn, const BigInt &balanceOut, - const BigInt &weightOut, unsigned int feeBips, const BigInt &amountIn) { BigInt BASE(FIXED_BASE); BigInt BASE_BIPS(10000); - BigInt weightRatio = (weightIn * BASE) / weightOut; - if (weightRatio > getMaxFieldElementAsBigInt(NUM_BITS_AMOUNT)) - { - return {false, 0}; - } BigInt fee = amountIn * feeBips / BASE_BIPS; BigInt denom = balanceIn + (amountIn - fee); if (denom == 0) @@ -1183,13 +1176,7 @@ static CalcOutResult calcOutGivenIn( return {false, 0}; } BigInt y = (balanceIn * BASE) / denom; - PowResult pow = pow_approx(toFieldElement(y), toFieldElement(weightRatio), CalcOutGivenInAMMGadget::numIterations); - if (!pow.valid) - { - return {false, 0}; - } - BigInt p = toBigInt(pow.value); - BigInt res = (balanceOut * (BASE - p)) / BASE; + BigInt res = (balanceOut * (BASE - y)) / BASE; return {true, res}; } @@ -1200,11 +1187,11 @@ struct CalcSpotResult }; static CalcSpotResult -calcSpotPrice(BigInt balanceIn, BigInt weightIn, BigInt balanceOut, BigInt weightOut, unsigned int feeBips) +calcSpotPrice(BigInt balanceIn, BigInt balanceOut, unsigned int feeBips) { BigInt BASE(FIXED_BASE); - BigInt numer = balanceIn * weightOut; - BigInt denom = balanceOut * weightIn; + BigInt numer = balanceIn; + BigInt denom = balanceOut; if (denom == 0) { return {false, 0}; @@ -1222,9 +1209,7 @@ static bool simulateRequireAMMFills( bool amm, unsigned int orderFeeBips, const BigInt &balanceIn, - const BigInt &weightIn, const BigInt &balanceOut, - const BigInt &weightOut, unsigned int feeBips, const BigInt &amountIn, const BigInt &amountOut) @@ -1238,20 +1223,20 @@ static bool simulateRequireAMMFills( { return false; } - if (weightIn == 0 || weightOut == 0) + if (balanceIn == 0 || balanceOut == 0) { return false; } - CalcOutResult expected = calcOutGivenIn(balanceIn, weightIn, balanceOut, weightOut, feeBips, amountIn); + CalcOutResult expected = calcOutGivenIn(balanceIn, balanceOut, feeBips, amountIn); if (!expected.valid || amountOut > expected.value) { return false; } - CalcSpotResult spotPriceBefore = calcSpotPrice(balanceIn, weightIn, balanceOut, weightOut, feeBips); + CalcSpotResult spotPriceBefore = calcSpotPrice(balanceIn, balanceOut, feeBips); CalcSpotResult spotPriceAfter = - calcSpotPrice(balanceIn + amountIn, weightIn, balanceOut - amountOut, weightOut, feeBips); + calcSpotPrice(balanceIn + amountIn, balanceOut - amountOut, feeBips); if (!spotPriceBefore.valid || !spotPriceAfter.valid || spotPriceAfter.value < spotPriceBefore.value) { return false; @@ -1273,9 +1258,7 @@ TEST_CASE("RequireAMMFills", "[RequireAMMFillsGadget]") bool _amm, unsigned int _orderFeeBips, const BigInt &_balanceIn, - const BigInt &_weightIn, const BigInt &_balanceOut, - const BigInt &_weightOut, unsigned int _feeBips, const BigInt &__amountIn, const BigInt &__amountOut, @@ -1295,9 +1278,7 @@ TEST_CASE("RequireAMMFills", "[RequireAMMFillsGadget]") VariableT amm = make_variable(pb, _amm ? 1 : 0, "amm"); VariableT orderFeeBips = make_variable(pb, _orderFeeBips, "amm"); VariableT balanceInBefore = make_variable(pb, toFieldElement(_balanceIn), "balanceInBefore"); - VariableT weightIn = make_variable(pb, toFieldElement(_weightIn), "weightIn"); VariableT balanceOutBefore = make_variable(pb, toFieldElement(_balanceOut), "balanceOut"); - VariableT weightOut = make_variable(pb, toFieldElement(_weightOut), "weightOut"); VariableT feeBips = make_variable(pb, FieldT(_feeBips), "feeBips"); VariableT amountIn = make_variable(pb, toFieldElement(_amountIn), "amountIn"); VariableT amountOut = make_variable(pb, toFieldElement(_amountOut), "amountOut"); @@ -1309,6 +1290,7 @@ TEST_CASE("RequireAMMFills", "[RequireAMMFillsGadget]") RequireAMMFillsGadget requireAMMFills( pb, constants, + amm, {amm, orderFeeBips, amountOut, @@ -1316,8 +1298,6 @@ TEST_CASE("RequireAMMFills", "[RequireAMMFillsGadget]") balanceInBefore, balanceOutAfter, balanceInAfter, - weightOut, - weightIn, feeBips}, amountIn, "calcOutGivenInAMM"); @@ -1325,7 +1305,7 @@ TEST_CASE("RequireAMMFills", "[RequireAMMFillsGadget]") requireAMMFills.generate_r1cs_witness(); bool expected = simulateRequireAMMFills( - _amm, _orderFeeBips, _balanceIn, _weightIn, _balanceOut, _weightOut, _feeBips, _amountIn, _amountOut); + _amm, _orderFeeBips, _balanceIn, _balanceOut, _feeBips, _amountIn, _amountOut); if (expectedSatisfied != ExpectedSatisfied::Automatic) { bool manualExpected = (expectedSatisfied == ExpectedSatisfied::Satisfied); @@ -1339,109 +1319,64 @@ TEST_CASE("RequireAMMFills", "[RequireAMMFillsGadget]") SECTION("Simple swap") { - requireAMMFillsChecked(true, 0, 1000, 1, 1000, 1, 10, 1, 1, ExpectedSatisfied::NotSatisfied); - requireAMMFillsChecked(true, 0, 1000, 1, 1000, 1, 10, 2, 1, ExpectedSatisfied::Satisfied); - } - - SECTION("AMM zero weights") - { - requireAMMFillsChecked(true, 0, 1000, 0, 1000, 0, 10, 2, 1, ExpectedSatisfied::NotSatisfied); - requireAMMFillsChecked(true, 0, 1000, 1, 1000, 0, 10, 2, 1, ExpectedSatisfied::NotSatisfied); - requireAMMFillsChecked(true, 0, 1000, 0, 1000, 1, 10, 2, 1, ExpectedSatisfied::NotSatisfied); - requireAMMFillsChecked(true, 0, 1000, 1, 1000, 1, 10, 2, 1, ExpectedSatisfied::Satisfied); - } - - SECTION("not AMM zero weights") - { - requireAMMFillsChecked(false, 0, 1000, 0, 1000, 0, 10, 2, 1, ExpectedSatisfied::Satisfied); - requireAMMFillsChecked(false, 0, 1000, 1, 1000, 0, 10, 2, 1, ExpectedSatisfied::Satisfied); - requireAMMFillsChecked(false, 0, 1000, 0, 1000, 1, 10, 2, 1, ExpectedSatisfied::Satisfied); - requireAMMFillsChecked(false, 0, 1000, 1, 1000, 1, 10, 2, 1, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(true, 0, 1000, 1000, 10, 1, 1, ExpectedSatisfied::NotSatisfied); + requireAMMFillsChecked(true, 0, 1000, 1000, 10, 2, 1, ExpectedSatisfied::Satisfied); } SECTION("not AMM 'invalid' fills") { - requireAMMFillsChecked(false, 0, 1000, 0, 1000, 0, 10, 1, 1, ExpectedSatisfied::Satisfied); - requireAMMFillsChecked(false, 1, 1000, 1, 1000, 0, 10, 1, 1, ExpectedSatisfied::Satisfied); - requireAMMFillsChecked(false, 1, 1000, 1, 1000, 0, 10, 2, 1, ExpectedSatisfied::Satisfied); - requireAMMFillsChecked(false, 1, 1000, 1, 1000, 0, 10, 1, 2, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(false, 0, 1000, 1000, 10, 1, 1, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(false, 1, 1000, 1000, 10, 1, 1, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(false, 1, 1000, 1000, 10, 2, 1, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(false, 1, 1000, 1000, 10, 1, 2, ExpectedSatisfied::Satisfied); } SECTION("AMM non-zero orderFeeBips") { - requireAMMFillsChecked(true, 1, 1000, 1, 1000, 1, 10, 2, 1, ExpectedSatisfied::NotSatisfied); - requireAMMFillsChecked(true, 0, 1000, 1, 1000, 1, 10, 2, 1, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(true, 1, 1000, 1000, 10, 2, 1, ExpectedSatisfied::NotSatisfied); + requireAMMFillsChecked(true, 0, 1000, 1000, 10, 2, 1, ExpectedSatisfied::Satisfied); } SECTION("Swap 0 -> 0") { - requireAMMFillsChecked(true, 0, 1, 1, 1, 1, 10, 0, 0, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(true, 0, 1, 1, 10, 0, 0, ExpectedSatisfied::Satisfied); } SECTION("Swap 1 -> 0") { - requireAMMFillsChecked(true, 0, 1000, 1, 1000, 1, 10, 1, 0, ExpectedSatisfied::Satisfied); + requireAMMFillsChecked(true, 0, 1000, 1000, 10, 1, 0, ExpectedSatisfied::Satisfied); } SECTION("Swap 0 -> 1") { - requireAMMFillsChecked(true, 0, 1000, 1, 1000, 1, 10, 0, 1, ExpectedSatisfied::NotSatisfied); - } - - SECTION("Max weights") - { - requireAMMFillsChecked(true, 0, 1000, max, 1000, max, 10, 2, 1); - requireAMMFillsChecked(true, 0, 1000, 1, 1000, max, 10, 2, 1); - requireAMMFillsChecked(true, 0, 1000, max, 1000, 1, 10, 2, 1); + requireAMMFillsChecked(true, 0, 1000, 1000, 10, 0, 1, ExpectedSatisfied::NotSatisfied); } SECTION("Balances zero") { - requireAMMFillsChecked(true, 0, 0, 1, 0, 1, 10, 0, 0); - requireAMMFillsChecked(true, 0, 1000, 1, 0, 1, 10, 0, 0); - requireAMMFillsChecked(true, 0, 0, 1, 1000, 1, 10, 0, 0); - } - - SECTION("Large ratio differences") - { - requireAMMFillsChecked(true, 0, max, BASE, 1, BASE, 10, 0, 0, ExpectedSatisfied::Satisfied); - requireAMMFillsChecked(true, 0, 1, BASE, max, BASE, 10, 0, 0, ExpectedSatisfied::Satisfied); - } - - SECTION("Ratio overflow") - { - requireAMMFillsChecked(true, 0, max, max, 1, 1, 10, 0, 0, ExpectedSatisfied::NotSatisfied); + requireAMMFillsChecked(true, 0, 0, 0, 10, 0, 0); + requireAMMFillsChecked(true, 0, 1000, 0, 10, 0, 0); + requireAMMFillsChecked(true, 0, 0, 1000, 10, 0, 0); } SECTION("Maxed out") { - requireAMMFillsChecked(true, 0, max, max, max, max, 10, 0, 0); - } - - SECTION("Large approximation errors") - { - requireAMMFillsChecked(true, 0, 1000, 4, 1000, 1, 10, 1000, 100); - } - - SECTION("Large approximation errors") - { - requireAMMFillsChecked(true, 0, 1000, 4, 1000, 1, 10, 1000, 100); + requireAMMFillsChecked(true, 0, max, max, 10, 0, 0); } SECTION("Fill limit check") { BigInt balanceIn = BASE * 1000; BigInt amountIn = BASE * 1; - BigInt weight = BASE * 1; unsigned int feeBips = 30; - CalcOutResult result = calcOutGivenIn(balanceIn, weight, balanceIn, weight, feeBips, amountIn); + CalcOutResult result = calcOutGivenIn(balanceIn, balanceIn, feeBips, amountIn); for (unsigned int p = 1; p < 200; p++) { BigInt amountOut = (amountIn * p) / 100; ExpectedSatisfied expected = (amountOut <= result.value) ? ExpectedSatisfied::Satisfied : ExpectedSatisfied::NotSatisfied; requireAMMFillsChecked( - true, 0, balanceIn, weight, balanceIn, weight, feeBips, amountIn, amountOut, expected); + true, 0, balanceIn, balanceIn, feeBips, amountIn, amountOut, expected); } } @@ -1452,14 +1387,12 @@ TEST_CASE("RequireAMMFills", "[RequireAMMFillsGadget]") bool amm = (rand() % 100) != 0; bool orderFeeBips = ((rand() % 100) == 0) ? (rand() % 64) : 0; BigInt balanceIn = getRandomFieldElementAsBigInt(NUM_BITS_AMOUNT); - BigInt weightIn = getRandomFieldElementAsBigInt(NUM_BITS_AMOUNT); BigInt balanceOut = getRandomFieldElementAsBigInt(NUM_BITS_AMOUNT); - BigInt weightOut = getRandomFieldElementAsBigInt(NUM_BITS_AMOUNT); unsigned int feeBips = rand() % 256; BigInt amountIn = getRandomFieldElementAsBigInt(NUM_BITS_AMOUNT); BigInt amountOut = getRandomFieldElementAsBigInt(NUM_BITS_AMOUNT); requireAMMFillsChecked( - amm, orderFeeBips, balanceIn, weightIn, balanceOut, weightOut, feeBips, amountIn, amountOut); + amm, orderFeeBips, balanceIn, balanceOut, feeBips, amountIn, amountOut); } } } diff --git a/packages/loopring_v3/circuit/test/MathTests.cpp b/packages/loopring_v3/circuit/test/MathTests.cpp index 9e541c342..cde19c073 100644 --- a/packages/loopring_v3/circuit/test/MathTests.cpp +++ b/packages/loopring_v3/circuit/test/MathTests.cpp @@ -1705,403 +1705,3 @@ TEST_CASE("ArraySelect", "[ArraySelectGadget]") } } } - -TEST_CASE("SignedAdd", "[SignedAddGadget]") -{ - unsigned int maxLength = 252; - for (unsigned int n = 1; n <= maxLength; n++) - { - DYNAMIC_SECTION("Bit-length: " << n) - { - auto addChecked = [n](const BigInt &_A, const BigInt &_B) { - protoboard pb; - - pb_variable A = make_variable(pb, toFieldElement(abs(_A)), "A"); - pb_variable B = make_variable(pb, toFieldElement(abs(_B)), "B"); - - Constants constants(pb, "constants"); - - SignedAddGadget addGadget( - pb, - constants, - SignedVariableT(_A > 0 ? constants._1 : constants._0, A), - SignedVariableT(_B > 0 ? constants._1 : constants._0, B), - n, - "addGadget"); - addGadget.generate_r1cs_constraints(); - addGadget.generate_r1cs_witness(); - - BigInt sum = _A + _B; - unsigned int sign = sum > 0 ? 1 : 0; - bool expectedSatisfied = (abs(sum) <= getMaxFieldElementAsBigInt(n)); - - REQUIRE(pb.is_satisfied() == expectedSatisfied); - if (expectedSatisfied) - { - REQUIRE(((pb.val(addGadget.result().value)) == toFieldElement(abs(sum)))); - REQUIRE(((pb.val(addGadget.result().sign)) == toFieldElement(sign))); - } - }; - - BigInt max = getMaxFieldElementAsBigInt(n); - BigInt halfMax = getMaxFieldElementAsBigInt(n - 1); - - SECTION("0 + 0") - { - addChecked(0, 0); - } - - SECTION("0 + 1") - { - addChecked(0, 1); - } - - SECTION("0 + (-1)") - { - addChecked(0, -1); - } - - SECTION("1 + (-1)") - { - addChecked(1, -1); - } - - SECTION("(-1) + 0") - { - addChecked(-1, 0); - } - - SECTION("(-1) + (-1)") - { - addChecked(-1, -1); - } - - SECTION("max + 0") - { - addChecked(max, 0); - } - - SECTION("max + (-1)") - { - addChecked(max, -1); - } - - SECTION("max + (-max)") - { - addChecked(max, -max); - } - - SECTION("(-max) + (-max)") - { - addChecked(-max, -max); - } - - SECTION("halfMax + halfMax + 1") - { - addChecked(halfMax, halfMax + 1); - } - - SECTION("max + 1 (overflow)") - { - addChecked(max, 1); - } - - SECTION("max + max (overflow)") - { - addChecked(max, max); - } - - SECTION("halfMax + halfMax + 2 (overflow)") - { - addChecked(halfMax, halfMax + 2); - } - } - } -} - -TEST_CASE("SignedSub", "[SignedSubGadget]") -{ - unsigned int maxLength = 252; - for (unsigned int n = 1; n <= maxLength; n++) - { - DYNAMIC_SECTION("Bit-length: " << n) - { - auto subChecked = [n](const BigInt &_A, const BigInt &_B) { - protoboard pb; - - pb_variable A = make_variable(pb, toFieldElement(abs(_A)), "A"); - pb_variable B = make_variable(pb, toFieldElement(abs(_B)), "B"); - - Constants constants(pb, "constants"); - - SignedSubGadget subGadget( - pb, - constants, - SignedVariableT(_A > 0 ? constants._1 : constants._0, A), - SignedVariableT(_B > 0 ? constants._1 : constants._0, B), - n, - "subGadget"); - subGadget.generate_r1cs_constraints(); - subGadget.generate_r1cs_witness(); - - BigInt sum = _A - _B; - unsigned int sign = sum > 0 ? 1 : 0; - bool expectedSatisfied = (abs(sum) <= getMaxFieldElementAsBigInt(n)); - - REQUIRE(pb.is_satisfied() == expectedSatisfied); - if (expectedSatisfied) - { - REQUIRE(((pb.val(subGadget.result().value)) == toFieldElement(abs(sum)))); - REQUIRE(((pb.val(subGadget.result().sign)) == toFieldElement(sign))); - } - }; - - BigInt max = getMaxFieldElementAsBigInt(n); - BigInt halfMax = getMaxFieldElementAsBigInt(n - 1); - - SECTION("0 - 0") - { - subChecked(0, 0); - } - - SECTION("0 - 1") - { - subChecked(0, 1); - } - - SECTION("0 - (-1)") - { - subChecked(0, -1); - } - - SECTION("1 - (-1)") - { - subChecked(1, -1); - } - - SECTION("(-1) - 0") - { - subChecked(-1, 0); - } - - SECTION("(-1) - (-1)") - { - subChecked(-1, -1); - } - - SECTION("max - 0") - { - subChecked(max, 0); - } - - SECTION("max - (-1)") - { - subChecked(max, -1); - } - - SECTION("max - (-max)") - { - subChecked(max, -max); - } - - SECTION("(-max) - (-max)") - { - subChecked(-max, -max); - } - - SECTION("max - max") - { - subChecked(max, max); - } - - SECTION("halfMax - (halfMax + 2)") - { - subChecked(halfMax, halfMax + 2); - } - } - } -} - -TEST_CASE("SignedMulDiv", "[SignedMulDivGadget]") -{ - unsigned int numRandom = 32; - unsigned int maxLength = 253 / 2; - unsigned int numIterations = 8; - for (unsigned int n = 1; n <= maxLength; n++) - { - DYNAMIC_SECTION("Bit-length: " << n) - { - auto mulDivChecked = - [n](const BigInt &_value, const BigInt &_numerator, const BigInt &_denominator, bool expectedSatisfied) { - protoboard pb; - - pb_variable value = make_variable(pb, toFieldElement(abs(_value)), "value"); - pb_variable numerator = make_variable(pb, toFieldElement(abs(_numerator)), "numerator"); - pb_variable denominator = make_variable(pb, toFieldElement(_denominator), "denominator"); - - Constants constants(pb, "constants"); - SignedMulDivGadget mulDivGadget( - pb, - constants, - SignedVariableT(_value > 0 ? constants._1 : constants._0, value), - SignedVariableT(_numerator > 0 ? constants._1 : constants._0, numerator), - denominator, - n, - n, - n, - "mulDivGadget"); - mulDivGadget.generate_r1cs_constraints(); - mulDivGadget.generate_r1cs_witness(); - - REQUIRE(pb.is_satisfied() == expectedSatisfied); - if (expectedSatisfied) - { - BigInt product = _value * _numerator; - BigInt remainder = product % _denominator; - BigInt result = product / _denominator; - - REQUIRE((pb.val(mulDivGadget.result().value) == toFieldElement(abs(result)))); - unsigned int sign = result > 0 ? 1 : 0; - REQUIRE((pb.val(mulDivGadget.result().sign) == toFieldElement(sign))); - } - }; - - BigInt max = getMaxFieldElementAsBigInt(n); - - SECTION("0 * 0 / 1 = 0") - { - mulDivChecked(0, 0, 1, true); - } - - SECTION("1 * 1 / 1 = 1") - { - mulDivChecked(1, 1, 1, true); - } - - SECTION("-1 * 1 / 1 = -1") - { - mulDivChecked(-1, 1, 1, true); - } - - SECTION("1 * -1 / 1 = -1") - { - mulDivChecked(1, -1, 1, true); - } - - SECTION("-1 * -1 / 1 = 1") - { - mulDivChecked(-1, -1, 1, true); - } - - SECTION("-3 * 1 / 2 = -1") - { - mulDivChecked(-3, 1, 2, true); - } - - SECTION("random value") - { - for (unsigned int j = 0; j < numRandom; j++) - { - BigInt value = toBigInt(getRandomFieldElement(n), (rand() % 2) == 0); - BigInt numerator = toBigInt(getRandomFieldElement(n), (rand() % 2) == 0); - BigInt denominator = toBigInt(getRandomFieldElement(n)); - bool expectedSatisfied = (denominator != 0); - mulDivChecked(value, numerator, denominator, expectedSatisfied); - } - } - } - } -} - -TEST_CASE("Power", "[PowerGadget]") -{ - unsigned int numRandom = 128; - unsigned int maxLength = 8; - for (unsigned int n = 3; n <= maxLength; n++) - { - DYNAMIC_SECTION("Iterations: " << n) - { - auto pow_approxChecked = [n](const FieldT &_x, const FieldT &_y) { - protoboard pb; - Constants constants(pb, "constants"); - - VariableT x = make_variable(pb, _x, "x"); - VariableT y = make_variable(pb, _y, "y"); - PowerGadget powerGadget(pb, constants, x, y, n, "powerGadget"); - powerGadget.generate_r1cs_witness(); - powerGadget.generate_r1cs_constraints(); - - PowResult expected = pow_approx(_x, _y, n); - REQUIRE(pb.is_satisfied() == expected.valid); - if (expected.valid) - { - REQUIRE((pb.val(powerGadget.result()) == expected.value)); - } - }; - - unsigned int n = NUM_BITS_AMOUNT; - BigInt BASE(FIXED_BASE); - FieldT fixedBase = FieldT(FIXED_BASE); - FieldT max = getMaxFieldElement(n); - - SECTION("1^1") - { - pow_approxChecked(fixedBase, fixedBase); - } - - SECTION("0^1") - { - pow_approxChecked(0, fixedBase); - } - - SECTION("1^0") - { - pow_approxChecked(0, fixedBase); - } - - SECTION("0.5^2") - { - pow_approxChecked(toFieldElement(BASE / 2), FieldT(2) * fixedBase); - } - - SECTION("0.5^0.5") - { - pow_approxChecked(toFieldElement(BASE / 2), toFieldElement(BASE / 2)); - } - - SECTION("0.333^0.5") - { - pow_approxChecked(toFieldElement(BASE / 3), toFieldElement(BASE / 2)); - } - - SECTION("0.5^0.333") - { - pow_approxChecked(toFieldElement(BASE / 2), toFieldElement(BASE / 3)); - } - - SECTION("0.5^3.333") - { - pow_approxChecked(toFieldElement(BASE / 2), toFieldElement(BASE * 10 / 3)); - } - - SECTION("0.333^3.333") - { - pow_approxChecked(toFieldElement(BASE / 3), toFieldElement(BASE * 10 / 3)); - } - - SECTION("random value") - { - for (unsigned int j = 0; j < numRandom; j++) - { - BigInt x = toBigInt(getRandomFieldElement(60)); - if (x > BASE) - { - x /= 2; - } - FieldT y = getRandomFieldElement(64); - pow_approxChecked(toFieldElement(x), y); - } - } - } - } -} diff --git a/packages/loopring_v3/circuit/test/TestUtils.h b/packages/loopring_v3/circuit/test/TestUtils.h index 4a49899a1..bd11e9b76 100644 --- a/packages/loopring_v3/circuit/test/TestUtils.h +++ b/packages/loopring_v3/circuit/test/TestUtils.h @@ -147,43 +147,4 @@ static UniversalTransaction getSpotTrade(const Block &block) return tx; } -struct PowResult -{ - bool valid; - FieldT value; -}; - -static PowResult pow_approx(const FieldT& _x, const FieldT& _y, unsigned int iterations) -{ - BigInt BASE(FIXED_BASE); - - BigInt x = toBigInt(_x) - BASE; - BigInt a = toBigInt(_y); - - std::vector bn = {BASE, BASE}; - std::vector cn = {BASE, a}; - std::vector xn = {BASE, x}; - BigInt sum = xn[0]*cn[0] + xn[1]*cn[1]; - for (unsigned int i = 2; i < iterations; i++) - { - BigInt v = a - bn[i-1]; - bn.push_back(bn[i-1] + BASE); - cn.push_back((cn[i-1] * v) / bn[i]); - xn.push_back((xn[i-1] * x) / BASE); - sum += xn[i]*cn[i]; - if(cn.back() > getMaxFieldElementAsBigInt(NUM_BITS_AMOUNT)) - { - return {false, 0}; - } - } - sum /= BASE; - - bool valid = true; - if (sum <= 0 || sum > getMaxFieldElementAsBigInt(NUM_BITS_AMOUNT)) - { - return {false, 0}; - } - return {valid, toFieldElement(sum)}; -} - #endif \ No newline at end of file diff --git a/packages/loopring_v3/contracts/amm/AmplifiedAmmController.sol b/packages/loopring_v3/contracts/amm/AmplifiedAmmController.sol new file mode 100644 index 000000000..4dadd7ca2 --- /dev/null +++ b/packages/loopring_v3/contracts/amm/AmplifiedAmmController.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../amm/LoopringAmmPool.sol"; +import "../lib/Claimable.sol"; +import "../lib/MathUint.sol"; +import "../lib/MathUint96.sol"; +import "../thirdparty/SafeCast.sol"; +import "./IAmmController.sol"; + + +/// @author Brecht Devos - +contract AmplifiedAmmController is IAmmController, Claimable +{ + using MathUint for uint; + using MathUint96 for uint96; + using SafeCast for uint; + + uint public constant AMPLIFICATION_FACTOR_BASE = (10 ** 18); + + uint public constant CURVE_CHANGE_MIN_DELAY = 7 days; + uint public constant CURVE_CHANGE_AUTH_WINDOW = 7 days; + + mapping (address => uint) amplificationFactors; + mapping (address => uint) public curveChangeAuthorization; + + function getInitialVirtualBalances( + uint96[] memory joinAmounts + ) + external + view + override + returns (uint96[] memory) + { + uint amplificationFactor = getAmplificationFactor(msg.sender); + uint96[] memory vTokenBalancesL2 = new uint96[](joinAmounts.length); + for (uint i = 0; i < joinAmounts.length; i++) { + vTokenBalancesL2[i] = (uint(joinAmounts[i]).mul(amplificationFactor) / AMPLIFICATION_FACTOR_BASE).toUint96(); + require(vTokenBalancesL2[i] > 0, "ZERO_VIRTUAL_BALANCE"); + } + return vTokenBalancesL2; + } + + function authorizeVirtualBalances( + uint96[] memory balances, + uint96[] memory /*vBalancesOld*/, + uint96[] memory vBalancesNew, + bytes memory /*data*/ + ) + external + override + returns (bool) + { + address pool = msg.sender; + + // Check if a curve change was explicitly authorized + if (consumeCurveChangeAuthorized(pool)) { + return true; + } + + if (amplificationFactors[pool] != 0) { + return false; + } + + // Special case: Always allow updating the virtual balances if the AF = 1 + for (uint i = 0; i < balances.length; i++) { + if (vBalancesNew[i] != balances[i]) { + return false; + } + } + + return true; + } + + function authorizeCurveChange(address pool) + external + onlyOwner + { + curveChangeAuthorization[pool] = block.timestamp + CURVE_CHANGE_MIN_DELAY; + } + + function setAmplificationFactor( + address pool, + uint amplificationFactor + ) + external + onlyOwner + { + amplificationFactors[pool] = amplificationFactor; + } + + function getAmplificationFactor(address pool) + public + view + returns (uint amplificationFactor) + { + amplificationFactor = amplificationFactors[pool]; + if (amplificationFactor == 0) { + amplificationFactor = AMPLIFICATION_FACTOR_BASE; + } + } + + function setupPool( + LoopringAmmPool pool, + AmmData.PoolConfig calldata config + ) + external + onlyOwner + { + pool.setupPool(config); + } + + function enterExitMode( + LoopringAmmPool pool, + bool enabled + ) + external + onlyOwner + { + pool.enterExitMode(enabled); + } + + // == Internal Functions == + + function consumeCurveChangeAuthorized(address pool) + internal + returns (bool authorized) + { + uint timestamp = curveChangeAuthorization[pool]; + authorized = (timestamp <= block.timestamp) && + (block.timestamp <= timestamp + CURVE_CHANGE_AUTH_WINDOW); + + // Remove authorization + if (authorized) { + delete curveChangeAuthorization[pool]; + } + } +} diff --git a/packages/loopring_v3/contracts/amm/IAmmController.sol b/packages/loopring_v3/contracts/amm/IAmmController.sol new file mode 100644 index 000000000..afe148885 --- /dev/null +++ b/packages/loopring_v3/contracts/amm/IAmmController.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; + + +/// @author Brecht Devos - +interface IAmmController +{ + /// @dev Called by the pool contract when a join is done on a pool without + /// any outstanding LP tokens (so in normal cases an empty pool). + /// @param joinAmounts The initial amounts in the pool + function getInitialVirtualBalances( + uint96[] memory joinAmounts + ) + external + view + returns (uint96[] memory); + + /// @dev Called by the pool contract when a SET_VIRTUAL_BALANCES operation is done + /// on the pool. + /// @param balances The balances in the pool + /// @param vBalancesOld The current virtual balances in the pool + /// @param vBalancesNew The new virtual balances in the pool + /// @param data Custom data + /// @return True if vBalancesNew can be used, else false + function authorizeVirtualBalances( + uint96[] memory balances, + uint96[] memory vBalancesOld, + uint96[] memory vBalancesNew, + bytes memory data + ) + external + returns (bool); +} diff --git a/packages/loopring_v3/contracts/amm/IAssetManager.sol b/packages/loopring_v3/contracts/amm/IAssetManager.sol new file mode 100644 index 000000000..3fe23e360 --- /dev/null +++ b/packages/loopring_v3/contracts/amm/IAssetManager.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; + + +/// @author Brecht Devos - +interface IAssetManager +{} diff --git a/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol b/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol index f902283dc..f0a12d801 100644 --- a/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol +++ b/packages/loopring_v3/contracts/amm/LoopringAmmPool.sol @@ -5,14 +5,15 @@ pragma experimental ABIEncoderV2; import "../aux/access/ITransactionReceiver.sol"; import "../core/iface/IAgentRegistry.sol"; -// import "../lib/Drainable.sol"; import "../lib/ReentrancyGuard.sol"; -import "./libamm/AmmTransactionReceiver.sol"; +import "../lib/TransferUtil.sol"; +import "./libamm/AmmAssetManagement.sol"; import "./libamm/AmmData.sol"; import "./libamm/AmmExitRequest.sol"; import "./libamm/AmmJoinRequest.sol"; import "./libamm/AmmPoolToken.sol"; import "./libamm/AmmStatus.sol"; +import "./libamm/AmmTransactionReceiver.sol"; import "./libamm/AmmWithdrawal.sol"; import "./PoolToken.sol"; @@ -24,24 +25,42 @@ contract LoopringAmmPool is ITransactionReceiver, ReentrancyGuard { - using AmmTransactionReceiver for AmmData.State; + using AmmAssetManagement for AmmData.State; using AmmJoinRequest for AmmData.State; using AmmExitRequest for AmmData.State; using AmmPoolToken for AmmData.State; using AmmStatus for AmmData.State; + using AmmTransactionReceiver for AmmData.State; using AmmWithdrawal for AmmData.State; + using TransferUtil for address; event PoolJoinRequested(AmmData.PoolJoin join); event PoolExitRequested(AmmData.PoolExit exit, bool force); event ForcedExitProcessed(address owner, uint96 burnAmount, uint96[] amounts); event Shutdown(uint timestamp); + IAmmController public immutable controller; + IAssetManager public immutable assetManager; + bool public immutable joinsDisabled; + modifier onlyFromExchangeOwner() { require(msg.sender == state.exchangeOwner, "UNAUTHORIZED"); _; } + modifier onlyFromAssetManager() + { + require(msg.sender == address(assetManager), "UNAUTHORIZED"); + _; + } + + modifier onlyFromController() + { + require(msg.sender == address(controller), "UNAUTHORIZED"); + _; + } + modifier onlyWhenOnline() { require(state.isOnline(), "NOT_ONLINE"); @@ -54,6 +73,18 @@ contract LoopringAmmPool is _; } + constructor( + IAmmController _controller, + IAssetManager _assetManager, + bool _joinsDisabled + ) + { + require(_controller != IAmmController(0), "ZERO_ADDRESS"); + controller = _controller; + assetManager = _assetManager; + joinsDisabled = _joinsDisabled; + } + function isOnline() public view @@ -68,9 +99,18 @@ contract LoopringAmmPool is external nonReentrant { + require(state.accountID == 0 || msg.sender == address(controller), "UNAUTHORIZED"); state.setupPool(config); } + function enterExitMode(bool enabled) + external + onlyFromController + { + require(state.exitMode != enabled, "INVALID_STATE"); + state.exitMode = enabled; + } + // Anyone is able to shut down the pool when requests aren't being processed any more. function shutdown(address exitOwner) external @@ -78,7 +118,16 @@ contract LoopringAmmPool is onlyWhenOnline nonReentrant { - state.shutdown(exitOwner); + state.shutdownByLP(exitOwner); + } + + function shutdownByController() + external + onlyWhenOnline + nonReentrant + onlyFromController + { + state.shutdownByController(); } function joinPool( @@ -129,7 +178,12 @@ contract LoopringAmmPool is // nonReentrant // Not needed, does not do any external calls // and can only be called by the exchange owner. { - state.onReceiveTransactions(txsData, callbackData); + AmmData.Settings memory settings = AmmData.Settings({ + controller: controller, + assetManager: assetManager, + joinsDisabled: joinsDisabled + }); + state.onReceiveTransactions(txsData, callbackData, settings); } function withdrawWhenOffline() @@ -140,10 +194,36 @@ contract LoopringAmmPool is state.withdrawWhenOffline(); } - function updateExchangeOwnerAndFeeBips() + function transferOut( + address to, + address token, + uint amount + ) external nonReentrant + onlyFromAssetManager + { + state.transferOut(to, token, amount); + } + + function setBalanceL1( + address token, + uint96 balance + ) + external + nonReentrant + onlyFromAssetManager + { + state.balancesL1[token] = balance; + } + + function getBalanceL1( + address token + ) + public + view + returns (uint96) { - state.updateExchangeOwnerAndFeeBips(); + return state.balancesL1[token]; } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmAssetManagement.sol b/packages/loopring_v3/contracts/amm/libamm/AmmAssetManagement.sol new file mode 100644 index 000000000..7af90a742 --- /dev/null +++ b/packages/loopring_v3/contracts/amm/libamm/AmmAssetManagement.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../../lib/ERC20.sol"; +import "../../lib/MathUint.sol"; +import "../../lib/TransferUtil.sol"; +import "./AmmData.sol"; + + +/// @title AmmAssetManagement +library AmmAssetManagement +{ + using TransferUtil for address; + + function deposit( + AmmData.State storage S, + address token, + uint96 amount + ) + public + { + if (amount == 0) { + return; + } + uint ethValue = 0; + if (token == address(0)) { + ethValue = amount; + } else { + ERC20(token).approve(address(S.exchange.getDepositContract()), amount); + } + S.exchange.deposit{value: ethValue}( + address(this), + address(this), + token, + amount, + new bytes(0) + ); + } + + function transferOut( + AmmData.State storage /*S*/, + address to, + address token, + uint amount + ) + public + { + token.transferOut(to, amount); + } +} diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmData.sol b/packages/loopring_v3/contracts/amm/libamm/AmmData.sol index cbef87457..2212498c6 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmData.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmData.sol @@ -5,6 +5,8 @@ pragma experimental ABIEncoderV2; import "../../core/iface/ExchangeData.sol"; import "../../core/iface/IExchangeV3.sol"; +import "../IAmmController.sol"; +import "../IAssetManager.sol"; import "./IAmmSharedConfig.sol"; @@ -18,7 +20,10 @@ library AmmData { NOOP, JOIN, - EXIT + EXIT, + SET_VIRTUAL_BALANCES, + DEPOSIT, + WITHDRAW } struct PoolConfig @@ -53,6 +58,22 @@ library AmmData uint32 validUntil; } + struct PoolVirtualBalances + { + uint96[] vBalancesNew; + bytes data; + } + + struct PoolDeposit + { + uint96[] amounts; + } + + struct PoolWithdrawal + { + uint96[] amounts; + } + struct PoolTx { PoolTxType txType; @@ -67,6 +88,13 @@ library AmmData uint16 tokenID; } + struct Settings + { + IAmmController controller; + IAssetManager assetManager; + bool joinsDisabled; + } + struct Context { // functional parameters @@ -83,6 +111,9 @@ library AmmData Token[] tokens; uint96[] tokenBalancesL2; + uint96[] vTokenBalancesL2; + + Settings settings; } struct State { @@ -91,9 +122,9 @@ library AmmData string symbol; uint _totalSupply; - mapping(address => uint) balanceOf; - mapping(address => mapping(address => uint)) allowance; - mapping(address => uint) nonces; + mapping (address => uint) balanceOf; + mapping (address => mapping (address => uint)) allowance; + mapping (address => uint) nonces; // AMM pool state variables IAmmSharedConfig sharedConfig; @@ -116,5 +147,9 @@ library AmmData // A map from a user to the forced exit. mapping (address => PoolExit) forcedExit; mapping (bytes32 => bool) approvedTx; + + mapping (address => uint96) balancesL1; + + bool exitMode; } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmDepositProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmDepositProcess.sol new file mode 100644 index 000000000..d952171f9 --- /dev/null +++ b/packages/loopring_v3/contracts/amm/libamm/AmmDepositProcess.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../../lib/ERC20.sol"; +import "../../lib/MathUint96.sol"; +import "../../lib/TransferUtil.sol"; +import "./AmmAssetManagement.sol"; +import "./AmmData.sol"; +import "./AmmPoolToken.sol"; +import "./AmmStatus.sol"; +import "./AmmUtil.sol"; +import "./AmmWithdrawal.sol"; + + +/// @title AmmDepositProcess +library AmmDepositProcess +{ + using AmmAssetManagement for AmmData.State; + using MathUint96 for uint96; + using TransferUtil for address; + + function processDeposit( + AmmData.State storage S, + AmmData.Context memory ctx, + AmmData.PoolDeposit memory poolDeposit + ) + internal + { + require(poolDeposit.amounts.length == ctx.tokens.length, "INVALID_DEPOSIT_DATA"); + for (uint i = 0; i < ctx.tokens.length; i++) { + uint96 amount = poolDeposit.amounts[i]; + if (amount > 0) { + address token = ctx.tokens[i].addr; + verifyDepositTx(ctx, ctx.tokens[i].tokenID, amount); + S.deposit(token, amount); + S.balancesL1[token] = S.balancesL1[token].sub(amount); + } + } + } + + function verifyDepositTx( + AmmData.Context memory ctx, + uint tokenID, + uint96 amount + ) + internal + view + { + // Verify deposit data + // Start by reading the first 27 bytes into packedData + uint txsDataPtr = ctx.txsDataPtr + 27; + // packedData: txType (1) | owner (20) | accountID (4) | tokenID (2) + uint packedData; + uint96 txAmount; + assembly { + packedData := calldataload(txsDataPtr) + txAmount := calldataload(add(txsDataPtr, 12)) + } + + require( + // txType == ExchangeData.TransactionType.DEPOSIT && + // owner == address(this) && + // accountID == ctx.accountID && + // tokenID == tokenID && + packedData & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffff == (uint(ExchangeData.TransactionType.DEPOSIT) << 208) | (uint(address(this)) << 48) | (uint(ctx.accountID) << 16) | uint(tokenID) && + amount == txAmount, + "INVALID_AMM_DEPOSIT_TX_DATA" + ); + + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; + } +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol index 32659ff9e..2371b3b8d 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmExitProcess.sol @@ -9,26 +9,26 @@ import "../../lib/EIP712.sol"; import "../../lib/ERC20SafeTransfer.sol"; import "../../lib/MathUint.sol"; import "../../lib/MathUint96.sol"; -import "../../lib/SignatureUtil.sol"; import "../../lib/TransferUtil.sol"; import "../../thirdparty/SafeCast.sol"; -import "./AmmUtil.sol"; import "./AmmData.sol"; import "./AmmExitRequest.sol"; import "./AmmPoolToken.sol"; +import "./AmmSignature.sol"; +import "./AmmUtil.sol"; /// @title AmmExitProcess library AmmExitProcess { using AmmPoolToken for AmmData.State; + using AmmSignature for bytes32; using AmmUtil for AmmData.Context; using AmmUtil for uint96; using ERC20SafeTransfer for address; using MathUint for uint; using MathUint96 for uint96; using SafeCast for uint; - using SignatureUtil for bytes32; using TransactionReader for ExchangeData.Block; using TransferUtil for address; @@ -49,14 +49,18 @@ library AmmExitProcess bool isForcedExit = false; if (signature.length == 0) { - bytes32 forcedExitHash = AmmExitRequest.hash(ctx.domainSeparator, S.forcedExit[exit.owner]); - if (txHash == forcedExitHash) { - delete S.forcedExit[exit.owner]; - S.forcedExitCount--; - isForcedExit = true; + if (S.exitMode) { + require(exit.fee == 0, "INVALID_FEE"); } else { - require(S.approvedTx[txHash], "INVALID_ONCHAIN_APPROVAL"); - delete S.approvedTx[txHash]; + bytes32 forcedExitHash = AmmExitRequest.hash(ctx.domainSeparator, S.forcedExit[exit.owner]); + if (txHash == forcedExitHash) { + delete S.forcedExit[exit.owner]; + S.forcedExitCount--; + isForcedExit = true; + } else { + require(S.approvedTx[txHash], "INVALID_ONCHAIN_APPROVAL"); + delete S.approvedTx[txHash]; + } } } else if (signature.length == 1) { ctx.verifySignatureL2(exit.owner, txHash, signature); @@ -197,6 +201,12 @@ library AmmExitProcess amounts[i] = amount; } + // Update virtual balances + uint newTotalSupply = ctx.totalSupply.sub(exit.burnAmount); + for (uint i = 0; i < ctx.tokens.length; i++) { + ctx.vTokenBalancesL2[i] = (uint(ctx.vTokenBalancesL2[i]).mul(newTotalSupply) / ctx.totalSupply).toUint96(); + } + return (true, amounts); } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol index be90556ef..0b11764cf 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmJoinProcess.sol @@ -8,11 +8,11 @@ import "../../core/impl/libtransactions/TransferTransaction.sol"; import "../../lib/EIP712.sol"; import "../../lib/MathUint.sol"; import "../../lib/MathUint96.sol"; -import "../../lib/SignatureUtil.sol"; import "../../thirdparty/SafeCast.sol"; import "./AmmData.sol"; import "./AmmJoinRequest.sol"; import "./AmmPoolToken.sol"; +import "./AmmSignature.sol"; import "./AmmUtil.sol"; @@ -20,12 +20,13 @@ import "./AmmUtil.sol"; library AmmJoinProcess { using AmmPoolToken for AmmData.State; + using AmmSignature for bytes32; + using AmmUtil for AmmData.State; using AmmUtil for AmmData.Context; using AmmUtil for uint96; using MathUint for uint; using MathUint96 for uint96; using SafeCast for uint; - using SignatureUtil for bytes32; using TransactionReader for ExchangeData.Block; // event JoinProcessed(address owner, uint96 mintAmount, uint96[] amounts); @@ -38,6 +39,7 @@ library AmmJoinProcess ) internal { + require(!ctx.settings.joinsDisabled, "JOINS_DISABLED"); require(join.validUntil >= block.timestamp, "EXPIRED"); bytes32 txHash = AmmJoinRequest.hash(ctx.domainSeparator, join); @@ -151,7 +153,7 @@ library AmmJoinProcess AmmData.PoolJoin memory join ) private - pure + view returns( bool slippageOK, uint96 mintAmount, @@ -162,6 +164,8 @@ library AmmJoinProcess amounts = new uint96[](ctx.tokens.length); if (ctx.totalSupply == 0) { + // Set virtual balances + ctx.vTokenBalancesL2 = ctx.settings.controller.getInitialVirtualBalances(join.joinAmounts); return(true, AmmData.POOL_TOKEN_BASE.toUint96(), join.joinAmounts); } @@ -169,14 +173,14 @@ library AmmJoinProcess bool initialized = false; for (uint i = 0; i < ctx.tokens.length; i++) { if (ctx.tokenBalancesL2[i] > 0) { - uint amountOut = uint(join.joinAmounts[i]) + uint maxMintAmount = uint(join.joinAmounts[i]) .mul(ctx.totalSupply) / uint(ctx.tokenBalancesL2[i]); if (!initialized) { initialized = true; - mintAmount = amountOut.toUint96(); - } else if (amountOut < mintAmount) { - mintAmount = amountOut.toUint96(); + mintAmount = maxMintAmount.toUint96(); + } else if (mintAmount > maxMintAmount) { + mintAmount = maxMintAmount.toUint96(); } } } @@ -188,8 +192,13 @@ library AmmJoinProcess // Calculate the amounts to deposit uint ratio = uint(AmmData.POOL_TOKEN_BASE).mul(mintAmount) / ctx.totalSupply; + uint newTotalSupply = ctx.totalSupply.add(mintAmount); for (uint i = 0; i < ctx.tokens.length; i++) { amounts[i] = (ratio.mul(ctx.tokenBalancesL2[i]) / AmmData.POOL_TOKEN_BASE).toUint96(); + + // Update virtual balances + ctx.vTokenBalancesL2[i] = (uint(ctx.vTokenBalancesL2[i]).mul(newTotalSupply) / ctx.totalSupply).toUint96(); + require(ctx.vTokenBalancesL2[i] > 0, "ZERO_VIRTUAL_BALANCE"); } slippageOK = (mintAmount >= join.mintMinAmount); diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmPoolToken.sol b/packages/loopring_v3/contracts/amm/libamm/AmmPoolToken.sol index 91b5ca760..522bd09d6 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmPoolToken.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmPoolToken.sol @@ -80,7 +80,7 @@ library AmmPoolToken uint256 deadline, bytes calldata signature ) - internal + public { require(deadline >= block.timestamp, 'EXPIRED'); diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmSignature.sol b/packages/loopring_v3/contracts/amm/libamm/AmmSignature.sol new file mode 100644 index 000000000..ffbefae4f --- /dev/null +++ b/packages/loopring_v3/contracts/amm/libamm/AmmSignature.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../../lib/SignatureUtil.sol"; + + +/// @title AmmSignature +library AmmSignature +{ + using SignatureUtil for bytes32; + + function verifySignature( + bytes32 signHash, + address signer, + bytes memory signature + ) + public + view + returns (bool) + { + return signHash.verifySignature(signer, signature); + } +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmStatus.sol b/packages/loopring_v3/contracts/amm/libamm/AmmStatus.sol index ef60b51df..faee45143 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmStatus.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmStatus.sol @@ -5,23 +5,13 @@ pragma experimental ABIEncoderV2; import "../../core/iface/IExchangeV3.sol"; import "../../lib/EIP712.sol"; -import "../../lib/ERC20.sol"; -import "../../lib/MathUint.sol"; -import "../../lib/MathUint96.sol"; -import "../../lib/SignatureUtil.sol"; import "./AmmData.sol"; -import "./AmmPoolToken.sol"; import "./IAmmSharedConfig.sol"; /// @title AmmStatus library AmmStatus { - using AmmPoolToken for AmmData.State; - using MathUint for uint; - using MathUint96 for uint96; - using SignatureUtil for bytes32; - event Shutdown(uint timestamp); function isOnline(AmmData.State storage S) @@ -42,29 +32,28 @@ library AmmStatus bytes(config.poolName).length > 0 && bytes(config.tokenSymbol).length > 0, "INVALID_NAME_OR_SYMBOL" ); + require(config.sharedConfig != address(0), "INVALID_SHARED_CONFIG"); require(config.tokens.length == config.weights.length, "INVALID_DATA"); require(config.tokens.length >= 2, "INVALID_DATA"); require(config.exchange != address(0), "INVALID_EXCHANGE"); - require(config.accountID != 0, "INVALID_ACCOUNT_ID"); - require(S.tokens.length == 0, "ALREADY_INITIALIZED"); + require(S._totalSupply == 0, "POOL_IN_USE"); - S.sharedConfig = IAmmSharedConfig(config.sharedConfig); IExchangeV3 exchange = IExchangeV3(config.exchange); - S.exchange = exchange; - S.exchangeOwner = exchange.owner(); - S.exchangeDomainSeparator = exchange.getDomainSeparator(); - S.accountID = config.accountID; - S.poolTokenID = exchange.getTokenID(address(this)); + S.feeBips = config.feeBips; S.domainSeparator = EIP712.hash(EIP712.Domain(config.poolName, "1.0.0", address(this))); - S.poolName = config.poolName; S.symbol = config.tokenSymbol; + S.sharedConfig = IAmmSharedConfig(config.sharedConfig); + S.exchange = exchange; + S.exchangeOwner = exchange.owner(); + S.exchangeDomainSeparator = exchange.getDomainSeparator(); + S.shutdownTimestamp = 0; + S.exitMode = false; + delete S.tokens; for (uint i = 0; i < config.tokens.length; i++) { - require(config.weights[i] > 0, "INVALID_TOKEN_WEIGHT"); - address token = config.tokens[i]; S.tokens.push(AmmData.Token({ addr: token, @@ -73,30 +62,64 @@ library AmmStatus })); } - // Mint all liquidity tokens to the pool account on L2 - S.balanceOf[address(this)] = AmmData.POOL_TOKEN_MINTED_SUPPLY; - S.allowance[address(this)][address(exchange.getDepositContract())] = type(uint256).max; - exchange.deposit( - address(this), // from - address(this), // to - address(this), // token - uint96(AmmData.POOL_TOKEN_MINTED_SUPPLY), - new bytes(0) - ); + if (S.accountID == 0) { // new pool + require(config.accountID != 0, "INVALID_ACCOUNT_ID"); + S.accountID = config.accountID; + S.poolTokenID = exchange.getTokenID(address(this)); + + // Mint all liquidity tokens to the pool account on L2 + S.balanceOf[address(this)] = AmmData.POOL_TOKEN_MINTED_SUPPLY; + S.allowance[address(this)][address(exchange.getDepositContract())] = type(uint256).max; + exchange.deposit( + address(this), // from + address(this), // to + address(this), // token + uint96(AmmData.POOL_TOKEN_MINTED_SUPPLY), + new bytes(0) + ); + } else {// reused pool + require(config.accountID == 0, "INCONSISTENT_ACCOUNT_ID"); + } } // Anyone is able to shut down the pool when requests aren't being processed any more. - function shutdown( + function shutdownByLP( AmmData.State storage S, address exitOwner ) public { - // If the exchange is in withdrawal mode allow the pool to be shutdown immediately if (!S.exchange.isInWithdrawalMode()) { uint64 validUntil = S.forcedExit[exitOwner].validUntil; require(validUntil > 0 && validUntil < block.timestamp, "INVALID_CHALLENGE"); + } + + _shutdown(S); + } + + function shutdownByController( + AmmData.State storage S + ) + public + { + _shutdown(S); + } + + // Anyone is able to update the cached exchange owner to the current owner. + function updateExchangeOwnerAndFeeBips(AmmData.State storage S) + public + { + S.exchangeOwner = S.exchange.owner(); + S.feeBips = S.exchange.getAmmFeeBips(); + } + function _shutdown( + AmmData.State storage S + ) + internal + { + // If the exchange is in withdrawal mode allow the pool to be shutdown immediately + if (!S.exchange.isInWithdrawalMode()) { uint size = S.tokens.length; for (uint i = 0; i < size; i++) { @@ -110,12 +133,4 @@ library AmmStatus S.shutdownTimestamp = uint64(block.timestamp); emit Shutdown(block.timestamp); } - - // Anyone is able to update the cached exchange owner to the current owner. - function updateExchangeOwnerAndFeeBips(AmmData.State storage S) - public - { - S.exchangeOwner = S.exchange.owner(); - S.feeBips = S.exchange.getAmmFeeBips(); - } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmTransactionReceiver.sol b/packages/loopring_v3/contracts/amm/libamm/AmmTransactionReceiver.sol index 4c008e83b..7b1786bf1 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmTransactionReceiver.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmTransactionReceiver.sol @@ -5,32 +5,42 @@ pragma experimental ABIEncoderV2; import "../../core/impl/libtransactions/BlockReader.sol"; import "../../lib/MathUint.sol"; +import "../../thirdparty/SafeCast.sol"; +import "./AmmDepositProcess.sol"; import "./AmmData.sol"; import "./AmmExitProcess.sol"; import "./AmmJoinProcess.sol"; import "./AmmPoolToken.sol"; import "./AmmUpdateProcess.sol"; +import "./AmmVirtualBalanceProcess.sol"; +import "./AmmWithdrawProcess.sol"; /// @title AmmTransactionReceiver library AmmTransactionReceiver { - using AmmExitProcess for AmmData.State; - using AmmJoinProcess for AmmData.State; - using AmmPoolToken for AmmData.State; - using AmmUpdateProcess for AmmData.Context; - using BlockReader for bytes; + using AmmDepositProcess for AmmData.State; + using AmmExitProcess for AmmData.State; + using AmmJoinProcess for AmmData.State; + using AmmPoolToken for AmmData.State; + using AmmUtil for AmmData.State; + using AmmUpdateProcess for AmmData.State; + using AmmVirtualBalanceProcess for AmmData.State; + using AmmWithdrawProcess for AmmData.State; + using BlockReader for bytes; + using MathUint for uint; + using MathUint96 for uint96; + using SafeCast for uint; function onReceiveTransactions( - AmmData.State storage S, - bytes calldata txsData, - bytes calldata callbackData + AmmData.State storage S, + bytes calldata txsData, + bytes calldata callbackData, + AmmData.Settings memory settings ) internal { - AmmData.Context memory ctx = _getContext(S, txsData); - - ctx.approveAmmUpdates(); + AmmData.Context memory ctx = _getContext(S, txsData, settings); _processPoolTx(S, ctx, callbackData); @@ -42,8 +52,9 @@ library AmmTransactionReceiver } function _getContext( - AmmData.State storage S, - bytes calldata txsData + AmmData.State storage S, + bytes calldata txsData, + AmmData.Settings memory settings ) private view @@ -64,7 +75,9 @@ library AmmTransactionReceiver feeBips: S.feeBips, totalSupply: S._totalSupply, tokens: S.tokens, - tokenBalancesL2: new uint96[](size) + tokenBalancesL2: new uint96[](size), + vTokenBalancesL2: new uint96[](size), + settings: settings }); } @@ -94,17 +107,21 @@ library AmmTransactionReceiver signature.offset := add(signature.offset, 0x20) } if (txType == AmmData.PoolTxType.JOIN) { - S.processJoin( - ctx, - abi.decode(data, (AmmData.PoolJoin)), - signature - ); + S.approveAmmUpdates(ctx, true); + S.processJoin(ctx, abi.decode(data, (AmmData.PoolJoin)), signature); + S.approveAmmUpdates(ctx, false); } else if (txType == AmmData.PoolTxType.EXIT) { - S.processExit( - ctx, - abi.decode(data, (AmmData.PoolExit)), - signature - ); + S.approveAmmUpdates(ctx, true); + S.processExit(ctx, abi.decode(data, (AmmData.PoolExit)), signature); + S.approveAmmUpdates(ctx, false); + } else if (txType == AmmData.PoolTxType.SET_VIRTUAL_BALANCES) { + S.approveAmmUpdates(ctx, true); + S.processSetVirtualBalances(ctx, abi.decode(data, (AmmData.PoolVirtualBalances))); + S.approveAmmUpdates(ctx, false); + } else if (txType == AmmData.PoolTxType.DEPOSIT) { + S.processDeposit(ctx, abi.decode(data, (AmmData.PoolDeposit))); + } else if (txType == AmmData.PoolTxType.WITHDRAW) { + S.processWithdrawal(ctx, abi.decode(data, (AmmData.PoolWithdrawal))); } else { revert("INVALID_POOL_TX_TYPE"); } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol index 6f4f0fcf6..a2f5dd286 100644 --- a/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol +++ b/packages/loopring_v3/contracts/amm/libamm/AmmUpdateProcess.sol @@ -7,15 +7,19 @@ import "../../aux/transactions/TransactionReader.sol"; import "../../core/impl/libtransactions/AmmUpdateTransaction.sol"; import "./AmmData.sol"; import "./AmmUtil.sol"; +import "../../lib/MathUint96.sol"; /// @title AmmUpdateProcess library AmmUpdateProcess { + using MathUint96 for uint96; using TransactionReader for ExchangeData.Block; function approveAmmUpdates( - AmmData.Context memory ctx + AmmData.State storage S, + AmmData.Context memory ctx, + bool start ) internal view @@ -54,17 +58,32 @@ library AmmUpdateProcess // update.tokenID == token.tokenID && // update.feeBips == ctx.feeBips && packedDataA & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff == - (uint(ExchangeData.TransactionType.AMM_UPDATE) << 216) | (uint(address(this)) << 56) | (ctx.accountID << 24) | (token.tokenID << 8) | ctx.feeBips && - // update.tokenWeight == token.weight - (packedDataB >> 128) & 0xffffffffffffffffffffffff == token.weight, + (uint(ExchangeData.TransactionType.AMM_UPDATE) << 216) | (uint(address(this)) << 56) | (ctx.accountID << 24) | (token.tokenID << 8) | ctx.feeBips, "INVALID_AMM_UPDATE_TX_DATA" ); - ctx.tokenBalancesL2[i] = /*tokenWeight*/uint96(packedDataB & 0xffffffffffffffffffffffff); + if (start) { + // Bring actual balances and virtual balances from L2 to L1 + ctx.tokenBalancesL2[i] = /*balance*/uint96(packedDataB & 0xffffffffffffffffffffffff); + ctx.vTokenBalancesL2[i] = /*vbalance*/uint96((packedDataB >> 128) & 0xffffffffffffffffffffffff); + } else { + // Verify new virtual balances are the same value as on L2. + require( + ctx.vTokenBalancesL2[i] == /*vbalance*/uint96((packedDataB >> 128) & 0xffffffffffffffffffffffff), + "INVALID_VIRTUAL_BALANCE_UPDATE" + ); + } txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; } + if (start && ctx.settings.assetManager != IAssetManager(0)) { + // Add the L1 balances on top of the balances on L2 + for (uint i = 0; i < ctx.tokens.length; i++) { + ctx.tokenBalancesL2[i] = ctx.tokenBalancesL2[i].add(S.balancesL1[ctx.tokens[i].addr]); + } + } + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE * ctx.tokens.length; } } diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmVirtualBalanceProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmVirtualBalanceProcess.sol new file mode 100644 index 000000000..68364d30a --- /dev/null +++ b/packages/loopring_v3/contracts/amm/libamm/AmmVirtualBalanceProcess.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "./AmmData.sol"; + + +/// @title AmmVirtualBalanceProcess +library AmmVirtualBalanceProcess +{ + function processSetVirtualBalances( + AmmData.State storage /* S */, + AmmData.Context memory ctx, + AmmData.PoolVirtualBalances memory poolVirtualBalances + ) + internal + { + require( + poolVirtualBalances.vBalancesNew.length == ctx.tokens.length, + "INVALID_DATA" + ); + require( + ctx.settings.controller.authorizeVirtualBalances( + ctx.tokenBalancesL2, + ctx.vTokenBalancesL2, + poolVirtualBalances.vBalancesNew, + poolVirtualBalances.data + ), + "NEW_VIRTUAL_BALANCES_NOT_AUTHORIZED" + ); + ctx.vTokenBalancesL2 = poolVirtualBalances.vBalancesNew; + } +} diff --git a/packages/loopring_v3/contracts/amm/libamm/AmmWithdrawProcess.sol b/packages/loopring_v3/contracts/amm/libamm/AmmWithdrawProcess.sol new file mode 100644 index 000000000..92d055d63 --- /dev/null +++ b/packages/loopring_v3/contracts/amm/libamm/AmmWithdrawProcess.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; +pragma experimental ABIEncoderV2; + +import "../../lib/ERC20.sol"; +import "../../lib/MathUint96.sol"; +import "../../lib/TransferUtil.sol"; +import "./AmmData.sol"; +import "./AmmPoolToken.sol"; +import "./AmmStatus.sol"; +import "./AmmUtil.sol"; + + +/// @title AmmWithdrawProcess +library AmmWithdrawProcess +{ + using MathUint96 for uint96; + using TransferUtil for address; + + function processWithdrawal( + AmmData.State storage S, + AmmData.Context memory ctx, + AmmData.PoolWithdrawal memory poolWithdrawal + ) + internal + { + require(ctx.settings.assetManager != IAssetManager(0), "CANNOT_WITHDRAW_FROM_POOL"); + require(poolWithdrawal.amounts.length == ctx.tokens.length, "INVALID_WITHDRAWAL_AMOUNTS"); + for (uint i = 0; i < ctx.tokens.length; i++) { + uint96 amount = poolWithdrawal.amounts[i]; + if (amount > 0) { + address token = ctx.tokens[i].addr; + verifyWithdrawalTx(ctx, ctx.tokens[i].tokenID, amount); + S.balancesL1[token] = S.balancesL1[token].add(amount); + } + } + } + + function verifyWithdrawalTx( + AmmData.Context memory ctx, + uint tokenID, + uint96 amount + ) + internal + view + { + bytes20 onchainDataHash = WithdrawTransaction.hashOnchainData( + 0, // Withdrawal needs to succeed no matter the gas coast + address(this), // Withdraw to this contract first + new bytes(0) + ); + + // Verify withdrawal data + // Start by reading the first 2 bytes into header + uint txsDataPtr = ctx.txsDataPtr + 2; + // header: txType (1) | type (1) + uint header; + // packedData: tokenID (2) | amount (12) | feeTokenID (2) | fee (2) + uint packedData; + bytes20 dataHash; + assembly { + header := calldataload( txsDataPtr ) + packedData := calldataload(add(txsDataPtr, 42)) + dataHash := and(calldataload(add(txsDataPtr, 78)), 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000) + } + + require( + // txType == ExchangeData.TransactionType.WITHDRAWAL && + // withdrawal.type == 1 && + header & 0xffff == (uint(ExchangeData.TransactionType.WITHDRAWAL) << 8) | 1 && + // withdrawal.tokenID == token.tokenID && + // withdrawal.amount == token.amount && + // withdrawal.fee == 0, + packedData & 0xffffffffffffffffffffffffffff0000ffff == (uint(tokenID) << 128) | (uint(amount) << 32) && + onchainDataHash == dataHash, + "INVALID_AMM_WITHDRAWAL_TX_DATA" + ); + + ctx.txsDataPtr += ExchangeData.TX_DATA_AVAILABILITY_SIZE; + } +} \ No newline at end of file diff --git a/packages/loopring_v3/contracts/aux/access/DelayedTransaction.sol b/packages/loopring_v3/contracts/aux/access/DelayedTransaction.sol index bd19dac61..59aef5b91 100644 --- a/packages/loopring_v3/contracts/aux/access/DelayedTransaction.sol +++ b/packages/loopring_v3/contracts/aux/access/DelayedTransaction.sol @@ -110,11 +110,11 @@ abstract contract DelayedTransaction is IDelayedTransaction, ReentrancyGuard { // First cache all transactions ids of the transactions we will remove uint[] memory transactionIds = new uint[](pendingTransactions.length); - for(uint i = 0; i < pendingTransactions.length; i++) { + for (uint i = 0; i < pendingTransactions.length; i++) { transactionIds[i] = pendingTransactions[i].id; } // Now remove all delayed transactions - for(uint i = 0; i < transactionIds.length; i++) { + for (uint i = 0; i < transactionIds.length; i++) { cancelTransactionInternal(transactionIds[i]); } } diff --git a/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol b/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol index 5975ac3f6..285774d70 100644 --- a/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol +++ b/packages/loopring_v3/contracts/aux/access/LoopringIOExchangeOwner.sol @@ -48,6 +48,7 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab { TransactionReceiverCallback[] callbacks; address[] receivers; + bool beforeBlockSubmission; } struct SubmitBlocksCallback @@ -143,11 +144,18 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab IExchangeV3(target).flashDeposit(flashDeposits); } + if (txReceiverCallbacks.beforeBlockSubmission) { + // Do transaction verifying blocks callbacks + _verifyTransactions(blocks, txReceiverCallbacks); + } + // Submit blocks target.fastCallAndVerify(gasleft(), 0, decompressed); - // Do transaction verifying blocks callbacks - _verifyTransactions(blocks, txReceiverCallbacks); + if (!txReceiverCallbacks.beforeBlockSubmission) { + // Do transaction verifying blocks callbacks + _verifyTransactions(blocks, txReceiverCallbacks); + } // Do post blocks callbacks _processCallbacks(submitBlocksCallbacks, false); @@ -202,7 +210,7 @@ contract LoopringIOExchangeOwner is SelectorBasedAccessManager, ERC1271, Drainab uint txIdx; bool approved; uint auxOffset; - for(uint j = 0; j < auxiliaryData.length; j++) { + for (uint j = 0; j < auxiliaryData.length; j++) { // Load the data from auxiliaryData, which is still encoded as calldata assembly { // Offset to auxiliaryData[j] diff --git a/packages/loopring_v3/contracts/aux/access/SelectorBasedAccessManager.sol b/packages/loopring_v3/contracts/aux/access/SelectorBasedAccessManager.sol index 0fc245268..c51006ba3 100644 --- a/packages/loopring_v3/contracts/aux/access/SelectorBasedAccessManager.sol +++ b/packages/loopring_v3/contracts/aux/access/SelectorBasedAccessManager.sol @@ -20,7 +20,7 @@ contract SelectorBasedAccessManager is Claimable ); address public immutable target; - mapping(address => mapping(bytes4 => bool)) public permissions; + mapping (address => mapping (bytes4 => bool)) public permissions; modifier withAccess(bytes4 selector) { diff --git a/packages/loopring_v3/contracts/aux/bridge/BatchDepositor.sol b/packages/loopring_v3/contracts/aux/bridge/BatchDepositor.sol index b7f342614..3ad74f553 100644 --- a/packages/loopring_v3/contracts/aux/bridge/BatchDepositor.sol +++ b/packages/loopring_v3/contracts/aux/bridge/BatchDepositor.sol @@ -56,7 +56,7 @@ abstract contract BatchDepositor is IBatchDepositor, ReentrancyGuard IDepositContract public immutable depositContract; mapping (uint => mapping (bytes32 => uint)) public pendingDeposits; - mapping (uint => mapping(uint => bool)) public withdrawn; + mapping (uint => mapping (uint => bool)) public withdrawn; // token -> tokenID mapping (address => uint16) public cachedTokenIDs; uint public batchIDGenerator; @@ -193,10 +193,10 @@ abstract contract BatchDepositor is IBatchDepositor, ReentrancyGuard IBatchDepositor.Deposit[] memory _deposits = depositsList[i]; for (uint j = 0; j < _deposits.length; j++) { deposit = _deposits[j]; - if(token != deposit.token) { + if (token != deposit.token) { token = deposit.token; tokenIdx = 0; - while(tokenIdx < numDistinctTokens && tokens[tokenIdx].token != token) { + while (tokenIdx < numDistinctTokens && tokens[tokenIdx].token != token) { tokenIdx++; } if (tokenIdx == numDistinctTokens) { @@ -223,7 +223,7 @@ abstract contract BatchDepositor is IBatchDepositor, ReentrancyGuard } // Do a normal deposit per token - for(uint i = 0; i < numDistinctTokens; i++) { + for (uint i = 0; i < numDistinctTokens; i++) { if (tokens[i].token == address(0)) { require(tokens[i].amount == msg.value || from == address(this), "INVALID_ETH_DEPOSIT"); } diff --git a/packages/loopring_v3/contracts/core/iface/ExchangeData.sol b/packages/loopring_v3/contracts/core/iface/ExchangeData.sol index 7f020c705..a64c2ca04 100644 --- a/packages/loopring_v3/contracts/core/iface/ExchangeData.sol +++ b/packages/loopring_v3/contracts/core/iface/ExchangeData.sol @@ -188,7 +188,7 @@ library ExchangeData bytes32 merkleRoot; // List of all blocks - mapping(uint => BlockInfo) blocks; + mapping (uint => BlockInfo) blocks; uint numBlocks; // List of all tokens diff --git a/packages/loopring_v3/contracts/lib/LPToken.sol b/packages/loopring_v3/contracts/lib/LPToken.sol index ec9d80c9e..4e21a9baf 100644 --- a/packages/loopring_v3/contracts/lib/LPToken.sol +++ b/packages/loopring_v3/contracts/lib/LPToken.sol @@ -18,10 +18,10 @@ contract LPToken is ERC20 string public symbol; uint8 public decimals; - uint public override totalSupply; - mapping(address => uint) public override balanceOf; - mapping(address => mapping(address => uint)) public override allowance; - mapping(address => uint) public nonces; + uint public override totalSupply; + mapping (address => uint) public override balanceOf; + mapping (address => mapping (address => uint)) public override allowance; + mapping (address => uint) public nonces; event Approval(address indexed owner, address indexed spender, uint value); event Transfer(address indexed from, address indexed to, uint value); diff --git a/packages/loopring_v3/contracts/test/LRCToken.sol b/packages/loopring_v3/contracts/test/LRCToken.sol index 2c3c348ff..12b86354a 100644 --- a/packages/loopring_v3/contracts/test/LRCToken.sol +++ b/packages/loopring_v3/contracts/test/LRCToken.sol @@ -60,7 +60,7 @@ library SafeMath { */ contract BasicToken is ERC20Basic { using SafeMath for uint; - mapping(address => uint) balances; + mapping (address => uint) balances; uint totalSupply_; /** * @dev total number of tokens in existence diff --git a/packages/loopring_v3/contracts/test/LoopringAmmPoolCopy.sol b/packages/loopring_v3/contracts/test/LoopringAmmPoolCopy.sol index 5eb53610e..b9545ebde 100644 --- a/packages/loopring_v3/contracts/test/LoopringAmmPoolCopy.sol +++ b/packages/loopring_v3/contracts/test/LoopringAmmPoolCopy.sol @@ -7,5 +7,10 @@ import "../amm/LoopringAmmPool.sol"; /// @title LoopringAmmPool contract LoopringAmmPoolCopy is LoopringAmmPool { - + constructor( + IAmmController _controller, + IAssetManager _assetManager, + bool _joinsDisabled + ) LoopringAmmPool(_controller, _assetManager, _joinsDisabled) + {} } diff --git a/packages/loopring_v3/contracts/test/TestAssetManager.sol b/packages/loopring_v3/contracts/test/TestAssetManager.sol new file mode 100644 index 000000000..e321fcb30 --- /dev/null +++ b/packages/loopring_v3/contracts/test/TestAssetManager.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2017 Loopring Technology Limited. +pragma solidity ^0.7.0; + +import "../lib/Claimable.sol"; +import "../lib/MathUint.sol"; +import "../lib/TransferUtil.sol"; +import "../amm/LoopringAmmPool.sol"; +import "../amm/IAssetManager.sol"; + + +/// @author Brecht Devos - +contract TestAssetManager is IAssetManager, Claimable +{ + using MathUint for uint; + using TransferUtil for address; + + mapping (address => mapping (address => uint)) public poolBalances; + + function getBalances( + address pool, + address[] memory tokens + ) + public + view + returns (uint[] memory) + { + uint[] memory balances = new uint[](tokens.length); + for (uint i = 0; i < tokens.length; i++) { + balances[i] = poolBalances[address(pool)][tokens[i]]; + } + return balances; + } + + function withdraw( + LoopringAmmPool pool, + address token, + uint amount + ) + external + onlyOwner + { + require(pool.isOnline(), "POOL_NOT_ONLINE"); + + uint balanceBefore = token.selfBalance(); + + pool.transferOut(address(this), token, amount); + poolBalances[address(pool)][token] = poolBalances[address(pool)][token].add(amount); + + uint balanceAfter = token.selfBalance(); + require (balanceAfter == balanceBefore.add(amount), "WITHDRAWAL_INCONSISTENT"); + } + + function deposit( + LoopringAmmPool pool, + address token, + uint amount + ) + external + onlyOwner + { + poolBalances[address(pool)][token] = poolBalances[address(pool)][token].sub(amount); + token.transferOut(address(pool), amount); + } + + function forceDeposit( + LoopringAmmPool pool, + address token + ) + external + { + require(!pool.isOnline(), "POOL_NOT_OFFLINE"); + + uint amount = poolBalances[address(pool)][token]; + delete poolBalances[address(pool)][token]; + + token.transferOut(address(pool), amount); + } + + receive() + external + payable + {} +} diff --git a/packages/loopring_v3/contracts/test/TestMigrationBridgeConnector.sol b/packages/loopring_v3/contracts/test/TestMigrationBridgeConnector.sol index 33b7fbf9b..950389e29 100644 --- a/packages/loopring_v3/contracts/test/TestMigrationBridgeConnector.sol +++ b/packages/loopring_v3/contracts/test/TestMigrationBridgeConnector.sol @@ -76,7 +76,7 @@ contract TestMigrationBridgeConnector is IBridgeConnector require(txs[i].token == settings.token, "WRONG_TOKEN_IN_GROUP"); address to = bridgeCall.owner; - if(bridgeCall.userData.length == 32) { + if (bridgeCall.userData.length == 32) { UserSettings memory userSettings = abi.decode(bridgeCall.userData, (UserSettings)); to = userSettings.to; } diff --git a/packages/loopring_v3/contracts/test/TestSwappperBridgeConnector.sol b/packages/loopring_v3/contracts/test/TestSwappperBridgeConnector.sol index cc77548cf..d4d51a8e0 100644 --- a/packages/loopring_v3/contracts/test/TestSwappperBridgeConnector.sol +++ b/packages/loopring_v3/contracts/test/TestSwappperBridgeConnector.sol @@ -82,7 +82,7 @@ contract TestSwappperBridgeConnector is IBridgeConnector uint ammountInInvalid = 0; for (uint i = 0; i < txs.length; i++) { bridgeTx = txs[i]; - if(valid[i] && bridgeTx.userData.length == 32) { + if (valid[i] && bridgeTx.userData.length == 32) { UserSettings memory userSettings = abi.decode(bridgeTx.userData, (UserSettings)); uint userAmountOut = uint(bridgeTx.amount).mul(amountOut) / amountInExpected; if (userAmountOut < userSettings.minAmountOut) { diff --git a/packages/loopring_v3/contracts/thirdparty/MockContract.sol b/packages/loopring_v3/contracts/thirdparty/MockContract.sol index 4237e2fbb..992f08bcc 100644 --- a/packages/loopring_v3/contracts/thirdparty/MockContract.sol +++ b/packages/loopring_v3/contracts/thirdparty/MockContract.sol @@ -86,17 +86,17 @@ contract MockContract is MockInterface { bytes4 public constant SENTINEL_ANY_MOCKS = hex"01"; // A linked list allows easy iteration and inclusion checks - mapping(bytes32 => bytes) calldataMocks; - mapping(bytes => MockType) calldataMockTypes; - mapping(bytes => bytes) calldataExpectations; - mapping(bytes => string) calldataRevertMessage; - mapping(bytes32 => uint) calldataInvocations; - - mapping(bytes4 => bytes4) methodIdMocks; - mapping(bytes4 => MockType) methodIdMockTypes; - mapping(bytes4 => bytes) methodIdExpectations; - mapping(bytes4 => string) methodIdRevertMessages; - mapping(bytes32 => uint) methodIdInvocations; + mapping (bytes32 => bytes) calldataMocks; + mapping (bytes => MockType) calldataMockTypes; + mapping (bytes => bytes) calldataExpectations; + mapping (bytes => string) calldataRevertMessage; + mapping (bytes32 => uint) calldataInvocations; + + mapping (bytes4 => bytes4) methodIdMocks; + mapping (bytes4 => MockType) methodIdMockTypes; + mapping (bytes4 => bytes) methodIdExpectations; + mapping (bytes4 => string) methodIdRevertMessages; + mapping (bytes32 => uint) methodIdInvocations; MockType fallbackMockType; bytes fallbackExpectation; @@ -261,7 +261,7 @@ contract MockContract is MockInterface { bytes memory nextMock = calldataMocks[MOCKS_LIST_START]; bytes32 mockHash = keccak256(nextMock); // We cannot compary bytes - while(mockHash != MOCKS_LIST_END_HASH) { + while (mockHash != MOCKS_LIST_END_HASH) { // Reset all mock maps calldataMockTypes[nextMock] = MockType.Return; calldataExpectations[nextMock] = hex""; @@ -278,7 +278,7 @@ contract MockContract is MockInterface { // Reset all any calldataMocks bytes4 nextAnyMock = methodIdMocks[SENTINEL_ANY_MOCKS]; - while(nextAnyMock != SENTINEL_ANY_MOCKS) { + while (nextAnyMock != SENTINEL_ANY_MOCKS) { bytes4 currentAnyMock = nextAnyMock; methodIdMockTypes[currentAnyMock] = MockType.Return; methodIdExpectations[currentAnyMock] = hex""; @@ -297,7 +297,7 @@ contract MockContract is MockInterface { } function useAllGas() private { - while(true) { + while (true) { bool s; assembly { //expensive call to EC multiply contract diff --git a/packages/loopring_v3/migrations/7_deploy_amm.js b/packages/loopring_v3/migrations/7_deploy_amm.js index 3f38bf309..de7edb0e8 100644 --- a/packages/loopring_v3/migrations/7_deploy_amm.js +++ b/packages/loopring_v3/migrations/7_deploy_amm.js @@ -7,25 +7,50 @@ const AmmJoinRequest = artifacts.require("AmmJoinRequest"); const AmmExitRequest = artifacts.require("AmmExitRequest"); const AmmStatus = artifacts.require("AmmStatus"); const AmmWithdrawal = artifacts.require("AmmWithdrawal"); +const AmmAssetManagement = artifacts.require("AmmAssetManagement"); +const AmmPoolToken = artifacts.require("AmmPoolToken"); +const AmmSignature = artifacts.require("AmmSignature"); +const AmplifiedAmmController = artifacts.require("AmplifiedAmmController"); module.exports = function(deployer, network, accounts) { if (network != "live" && network != "live-fork") { deployer.then(async () => { + await deployer.deploy(AmplifiedAmmController); + + await deployer.deploy(AmmSignature); + await deployer.deploy(AmmPoolToken); + await deployer.deploy(AmmAssetManagement); await deployer.deploy(AmmJoinRequest); await deployer.deploy(AmmExitRequest); await deployer.deploy(AmmStatus); await deployer.deploy(AmmWithdrawal); + await deployer.link(AmmSignature, LoopringAmmPool); + await deployer.link(AmmPoolToken, LoopringAmmPool); + await deployer.link(AmmAssetManagement, LoopringAmmPool); await deployer.link(AmmJoinRequest, LoopringAmmPool); await deployer.link(AmmExitRequest, LoopringAmmPool); await deployer.link(AmmStatus, LoopringAmmPool); await deployer.link(AmmWithdrawal, LoopringAmmPool); - await deployer.deploy(LoopringAmmPool); + await deployer.deploy( + LoopringAmmPool, + AmplifiedAmmController.address, + "0x" + "00".repeat(20), + false + ); + await deployer.link(AmmSignature, LoopringAmmPoolCopy); + await deployer.link(AmmPoolToken, LoopringAmmPoolCopy); + await deployer.link(AmmAssetManagement, LoopringAmmPoolCopy); await deployer.link(AmmJoinRequest, LoopringAmmPoolCopy); await deployer.link(AmmExitRequest, LoopringAmmPoolCopy); await deployer.link(AmmStatus, LoopringAmmPoolCopy); await deployer.link(AmmWithdrawal, LoopringAmmPoolCopy); - await deployer.deploy(LoopringAmmPoolCopy); + await deployer.deploy( + LoopringAmmPoolCopy, + AmplifiedAmmController.address, + "0x" + "00".repeat(20), + false + ); await deployer.deploy(LoopringAmmSharedConfig); const loopringAmmSharedConfig = await LoopringAmmSharedConfig.deployed(); diff --git a/packages/loopring_v3/operator/state.py b/packages/loopring_v3/operator/state.py index d1e03172e..ce18beed5 100644 --- a/packages/loopring_v3/operator/state.py +++ b/packages/loopring_v3/operator/state.py @@ -181,7 +181,7 @@ def getBalanceLeaf(self, address): def getBalance(self, address): return self.getBalanceLeaf(address).balance - def updateBalance(self, tokenID, deltaBalance): + def updateBalance(self, tokenID, deltaBalance, weight_delta = None): # Make sure the leaf exists in our map if not(str(tokenID) in self._balancesLeafs): self._balancesLeafs[str(tokenID)] = BalanceLeaf() @@ -190,6 +190,8 @@ def updateBalance(self, tokenID, deltaBalance): rootBefore = self._balancesTree._root self._balancesLeafs[str(tokenID)].balance = str(int(self._balancesLeafs[str(tokenID)].balance) + int(deltaBalance)) + if weight_delta is not None: + self._balancesLeafs[str(tokenID)].weightAMM = str(int(self._balancesLeafs[str(tokenID)].weightAMM) + int(weight_delta)) balancesAfter = copyBalanceInfo(self._balancesLeafs[str(tokenID)]) proof = self._balancesTree.createProof(tokenID) @@ -200,7 +202,7 @@ def updateBalance(self, tokenID, deltaBalance): rootBefore, rootAfter, balancesBefore, balancesAfter) - def updateBalanceAndStorage(self, tokenID, storageID, filled, delta_balance, weight = None): + def updateBalanceAndStorage(self, tokenID, storageID, filled, delta_balance, weight_delta = None): # Make sure the leaf exist in our map if not(str(tokenID) in self._balancesLeafs): self._balancesLeafs[str(tokenID)] = BalanceLeaf() @@ -211,8 +213,8 @@ def updateBalanceAndStorage(self, tokenID, storageID, filled, delta_balance, wei # Update filled amounts storageUpdate = self._balancesLeafs[str(tokenID)].updateStorage(storageID, filled) self._balancesLeafs[str(tokenID)].balance = str(int(self._balancesLeafs[str(tokenID)].balance) + int(delta_balance)) - if weight is not None: - self._balancesLeafs[str(tokenID)].weightAMM = str(weight) + if weight_delta is not None: + self._balancesLeafs[str(tokenID)].weightAMM = str(int(self._balancesLeafs[str(tokenID)].weightAMM) + int(weight_delta)) #print("str(delta_balance): " + str(delta_balance)) #print("endBalance: " + self._balancesLeafs[str(tokenID)].balance) @@ -449,6 +451,7 @@ def executeTransaction(self, context, txInput): newState.TXV_BALANCE_A_S_BALANCE = None newState.TXV_BALANCE_A_S_WEIGHT = None newState.TXV_BALANCE_A_B_BALANCE = None + newState.TXV_BALANCE_A_B_WEIGHT = None newState.TXV_STORAGE_A_ADDRESS = None newState.TXV_STORAGE_A_DATA = None newState.TXV_STORAGE_A_STORAGEID = None @@ -460,7 +463,9 @@ def executeTransaction(self, context, txInput): newState.TXV_ACCOUNT_B_NONCE = None newState.TXV_BALANCE_B_S_ADDRESS = None newState.TXV_BALANCE_B_S_BALANCE = None + newState.TXV_BALANCE_B_S_WEIGHT = None newState.TXV_BALANCE_B_B_BALANCE = None + newState.TXV_BALANCE_B_B_WEIGHT = None newState.TXV_STORAGE_B_ADDRESS = None newState.TXV_STORAGE_B_DATA = None newState.TXV_STORAGE_B_STORAGEID = None @@ -559,9 +564,11 @@ def executeTransaction(self, context, txInput): newState.TXV_BALANCE_A_S_ADDRESS = ring.orderA.tokenS newState.TXV_BALANCE_A_S_BALANCE = -fillA.S + newState.TXV_BALANCE_A_S_WEIGHT = -fillA.S if ring.orderA.amm else 0 newState.TXV_BALANCE_B_S_ADDRESS = ring.orderA.tokenB newState.TXV_BALANCE_A_B_BALANCE = fillA.B - fee_A + newState.TXV_BALANCE_A_B_WEIGHT = fillA.B if ring.orderA.amm else 0 newState.TXV_STORAGE_A_ADDRESS = ring.orderA.storageID newState.TXV_STORAGE_A_DATA = filled_A + (fillA.B if ring.orderA.fillAmountBorS else fillA.S) @@ -573,9 +580,11 @@ def executeTransaction(self, context, txInput): newState.TXV_BALANCE_B_S_ADDRESS = ring.orderB.tokenS newState.TXV_BALANCE_B_S_BALANCE = -fillB.S + newState.TXV_BALANCE_B_S_WEIGHT = -fillB.S if ring.orderB.amm else 0 newState.TXV_BALANCE_A_S_ADDRESS = ring.orderB.tokenB newState.TXV_BALANCE_B_B_BALANCE = fillB.B - fee_B + newState.TXV_BALANCE_B_B_WEIGHT = fillB.B if ring.orderB.amm else 0 newState.TXV_STORAGE_B_ADDRESS = ring.orderB.storageID newState.TXV_STORAGE_B_DATA = filled_B + (fillB.B if ring.orderB.fillAmountBorS else fillB.S) @@ -659,7 +668,8 @@ def executeTransaction(self, context, txInput): newState.TXV_STORAGE_A_DATA = 1 newState.TXV_STORAGE_A_STORAGEID = txInput.storageID if not isProtocolfeeWithdrawal and int(txInput.type) == 2: - newState.TXV_BALANCE_A_S_WEIGHT = 0 + balanceLeafA_S = accountA.getBalanceLeaf(newState.TXV_BALANCE_A_S_ADDRESS) + newState.TXV_BALANCE_A_S_WEIGHT = -int(balanceLeafA_S.weightAMM) newState.balanceDeltaA_O = feeValue @@ -711,7 +721,10 @@ def executeTransaction(self, context, txInput): newState.TXV_ACCOUNT_A_NONCE = 1 newState.TXV_ACCOUNT_A_FEEBIPSAMM = txInput.feeBips - newState.TXV_BALANCE_A_S_WEIGHT = txInput.tokenWeight + + accountA = self.getAccount(newState.TXV_ACCOUNT_A_ADDRESS) + balanceLeafA_S = accountA.getBalanceLeaf(newState.TXV_BALANCE_A_S_ADDRESS) + newState.TXV_BALANCE_A_S_WEIGHT = int(txInput.tokenWeight) - int(balanceLeafA_S.weightAMM) context.numConditionalTransactions = context.numConditionalTransactions + 1 @@ -736,9 +749,10 @@ def executeTransaction(self, context, txInput): balanceLeafA_S = accountA.getBalanceLeaf(newState.TXV_BALANCE_A_S_ADDRESS) newState.TXV_BALANCE_A_S_BALANCE = setValue(newState.TXV_BALANCE_A_S_BALANCE, 0) - newState.TXV_BALANCE_A_S_WEIGHT = setValue(newState.TXV_BALANCE_A_S_WEIGHT, balanceLeafA_S.weightAMM) + newState.TXV_BALANCE_A_S_WEIGHT = setValue(newState.TXV_BALANCE_A_S_WEIGHT, 0) newState.TXV_BALANCE_A_B_BALANCE = setValue(newState.TXV_BALANCE_A_B_BALANCE, 0) + newState.TXV_BALANCE_A_B_WEIGHT = setValue(newState.TXV_BALANCE_A_B_WEIGHT, 0) newState.TXV_STORAGE_A_ADDRESS = setValue(newState.TXV_STORAGE_A_ADDRESS, 0) storageA = balanceLeafA_S.getStorage(newState.TXV_STORAGE_A_ADDRESS) @@ -773,7 +787,8 @@ def executeTransaction(self, context, txInput): ) balanceUpdateB_A = accountA.updateBalance( newState.TXV_BALANCE_B_S_ADDRESS, - newState.TXV_BALANCE_A_B_BALANCE + newState.TXV_BALANCE_A_B_BALANCE, + newState.TXV_BALANCE_A_B_WEIGHT ) accountA.owner = newState.TXV_ACCOUNT_A_OWNER @@ -798,8 +813,10 @@ def executeTransaction(self, context, txInput): balanceLeafB_S = accountB.getBalanceLeaf(newState.TXV_BALANCE_B_S_ADDRESS) newState.TXV_BALANCE_B_S_BALANCE = setValue(newState.TXV_BALANCE_B_S_BALANCE, 0) + newState.TXV_BALANCE_B_S_WEIGHT = setValue(newState.TXV_BALANCE_B_S_WEIGHT, 0) newState.TXV_BALANCE_B_B_BALANCE = setValue(newState.TXV_BALANCE_B_B_BALANCE, 0) + newState.TXV_BALANCE_B_B_WEIGHT = setValue(newState.TXV_BALANCE_B_B_WEIGHT, 0) newState.TXV_STORAGE_B_ADDRESS = setValue(newState.TXV_STORAGE_B_ADDRESS, 0) storageB = balanceLeafB_S.getStorage(newState.TXV_STORAGE_B_ADDRESS) @@ -817,11 +834,13 @@ def executeTransaction(self, context, txInput): newState.TXV_BALANCE_B_S_ADDRESS, newState.TXV_STORAGE_B_STORAGEID, newState.TXV_STORAGE_B_DATA, - newState.TXV_BALANCE_B_S_BALANCE + newState.TXV_BALANCE_B_S_BALANCE, + newState.TXV_BALANCE_B_S_WEIGHT ) balanceUpdateB_B = accountB.updateBalance( newState.TXV_BALANCE_A_S_ADDRESS, - newState.TXV_BALANCE_B_B_BALANCE + newState.TXV_BALANCE_B_B_BALANCE, + newState.TXV_BALANCE_B_B_WEIGHT ) accountB.owner = newState.TXV_ACCOUNT_B_OWNER diff --git a/packages/loopring_v3/package.json b/packages/loopring_v3/package.json index fa4e4f4cc..c364e9f0e 100644 --- a/packages/loopring_v3/package.json +++ b/packages/loopring_v3/package.json @@ -1,6 +1,6 @@ { "name": "loopring_v3", - "version": "3.0.0", + "version": "3.8.0", "description": "Loopring DEX protocol version 3.x", "main": "index.js", "directories": { diff --git a/packages/loopring_v3/test/ammUtils.ts b/packages/loopring_v3/test/ammUtils.ts index 8ccc17109..327201044 100644 --- a/packages/loopring_v3/test/ammUtils.ts +++ b/packages/loopring_v3/test/ammUtils.ts @@ -10,7 +10,10 @@ import { logDebug } from "./logs"; export enum PoolTransactionType { NOOP, JOIN, - EXIT + EXIT, + SET_VIRTUAL_BALANCES, + DEPOSIT, + WITHDRAW } export interface PoolJoin { @@ -47,6 +50,46 @@ export interface PoolExit { numTxs?: number; } +export interface PoolVirtualBalances { + txType?: "SetVirtualBalances"; + poolAddress: string; + vBalances: BN[]; + data: string; + + owner?: string; + signature?: string; + authMethod?: AuthMethod; + actualAmounts?: BN[]; + txIdx?: number; + numTxs?: number; +} + +export interface PoolDeposit { + txType?: "Deposit"; + poolAddress: string; + amounts: BN[]; + + owner?: string; + signature?: string; + authMethod?: AuthMethod; + actualAmounts?: BN[]; + txIdx?: number; + numTxs?: number; +} + +export interface PoolWithdrawal { + txType?: "Withdrawal"; + poolAddress: string; + amounts: BN[]; + + owner?: string; + signature?: string; + authMethod?: AuthMethod; + actualAmounts?: BN[]; + txIdx?: number; + numTxs?: number; +} + export interface PoolTransaction { txType: number; data: string; @@ -83,7 +126,12 @@ export interface Permit { deadline: BN; } -type TxType = PoolJoin | PoolExit; +type TxType = + | PoolJoin + | PoolExit + | PoolVirtualBalances + | PoolDeposit + | PoolWithdrawal; export namespace PoolJoinUtils { export function toTypedData(join: PoolJoin, verifyingContract: string) { @@ -223,10 +271,12 @@ export class AmmPool { public feeBips: number; public tokens: string[]; - public weights: BN[]; + public vTokenBalancesL2: BN[]; + public amplificationFactor: BN; public POOL_TOKEN_BASE: BN = new BN("10000000000"); public POOL_TOKEN_MINTED_SUPPLY: BN = new BN("79228162514264337593543950335"); // uint96(-1) + public AMPLIFICATION_FACTOR_BASE = new BN("1000000000000000000"); public L2_SIGNATURE: string = "0x10"; @@ -241,18 +291,32 @@ export class AmmPool { public async setupPool( sharedConfig: any, tokens: string[], - weights: BN[], - feeBips: number + vTokenBalancesL2: BN[], + feeBips: number, + amplificationFactor: BN, + controllerAddress?: string, + assetManagerAddress?: string ) { this.sharedConfig = sharedConfig; this.feeBips = feeBips; this.tokens = tokens; - this.weights = weights; + this.vTokenBalancesL2 = vTokenBalancesL2; + this.amplificationFactor = amplificationFactor; this.totalSupply = new BN(0); + controllerAddress = controllerAddress + ? controllerAddress + : Constants.zeroAddress; + + console.log("controllerAddress: " + controllerAddress); + const AmmPool = artifacts.require("LoopringAmmPool"); - this.contract = await AmmPool.new(); + this.contract = await AmmPool.new( + controllerAddress, + assetManagerAddress, + false + ); // Create the AMM account const owner = this.contract.address; @@ -274,8 +338,8 @@ export class AmmPool { // Collect token addresses const strWeights: string[] = []; - for (const weight of weights) { - strWeights.push(weight.toString(10)); + for (const vTokenBalanceL2 of vTokenBalancesL2) { + strWeights.push(vTokenBalanceL2.toString(10)); } // Register the pool token @@ -447,6 +511,79 @@ export class AmmPool { return exit; } + public async setVirtualBalances(vBalances: BN[], data: string = "0x") { + const vb: PoolVirtualBalances = { + txType: "SetVirtualBalances", + poolAddress: this.contract.address, + signature: "0x00", + owner: Constants.zeroAddress, + vBalances, + data + }; + + await this.process(vb, undefined); + + return vb; + } + + public async deposit(amounts: BN[]) { + const deposit: PoolDeposit = { + txType: "Deposit", + poolAddress: this.contract.address, + amounts: amounts, + signature: "0x00", + owner: Constants.zeroAddress + }; + + await this.process(deposit, undefined); + + return deposit; + } + + public async withdraw(amounts: BN[]) { + const withdrawal: PoolWithdrawal = { + txType: "Withdrawal", + poolAddress: this.contract.address, + amounts: amounts, + signature: "0x00", + owner: Constants.zeroAddress + }; + + await this.process(withdrawal, undefined); + + return withdrawal; + } + + public async getBalancesL2() { + // Test framework not smart enough to immediately have the new balances after submitting a tx. + // Have to create a block to get the current offchain balance. + await this.ctx.submitTransactions(); + + const owner = this.contract.address; + const tokenBalancesL2: BN[] = []; + for (let i = 0; i < this.tokens.length; i++) { + tokenBalancesL2.push( + await this.ctx.getOffchainBalance(owner, this.tokens[i]) + ); + } + return tokenBalancesL2; + } + + public async getVirtualBalancesL2() { + // Test framework not smart enough to immediately have the new balances after submitting a tx. + // Have to create a block to get the current offchain balance. + await this.ctx.submitTransactions(); + + const owner = this.contract.address; + const vTokenBalancesL2: BN[] = []; + for (let i = 0; i < this.tokens.length; i++) { + vTokenBalancesL2.push( + await this.ctx.getOffchainVirtualBalance(owner, this.tokens[i]) + ); + } + return vTokenBalancesL2; + } + public async prePoolTransactions() { // Test framework not smart enough to immediately have the new balances after submitting a tx. // Have to create a block to get the current offchain balance. @@ -458,6 +595,10 @@ export class AmmPool { this.tokenBalancesL2.push( await this.ctx.getOffchainBalance(owner, this.tokens[i]) ); + this.vTokenBalancesL2[i] = await this.ctx.getOffchainVirtualBalance( + owner, + this.tokens[i] + ); } } @@ -468,15 +609,22 @@ export class AmmPool { let numTxs = 0; - for (let i = 0; i < this.tokens.length; i++) { - await this.ctx.requestAmmUpdate( - owner, - this.tokens[i], - this.feeBips, - this.weights[i], - { authMethod: AuthMethod.NONE } - ); - numTxs++; + const doAmmUpdates = + transaction.txType === "Join" || + transaction.txType === "Exit" || + transaction.txType === "SetVirtualBalances"; + + if (doAmmUpdates) { + for (let i = 0; i < this.tokens.length; i++) { + await this.ctx.requestAmmUpdate( + owner, + this.tokens[i], + this.feeBips, + this.vTokenBalancesL2[i], + { authMethod: AuthMethod.NONE } + ); + numTxs++; + } } if (transaction.signature === this.L2_SIGNATURE) { @@ -499,6 +647,13 @@ export class AmmPool { if (poolTotal.eq(new BN(0))) { mintAmount = this.POOL_TOKEN_BASE; amounts.push(...join.joinAmounts); + + // Set virtual balances + for (let i = 0; i < this.tokens.length; i++) { + this.vTokenBalancesL2[i] = join.joinAmounts[i] + .mul(this.amplificationFactor) + .div(this.AMPLIFICATION_FACTOR_BASE); + } } else { // Calculate the amount of liquidity tokens that should be minted let initialValueSet = false; @@ -522,10 +677,16 @@ export class AmmPool { // Calculate the amounts to deposit let ratio = mintAmount.mul(this.POOL_TOKEN_BASE).div(poolTotal); + const newTotalSupply = poolTotal.add(mintAmount); for (let i = 0; i < this.tokens.length; i++) { amounts.push( this.tokenBalancesL2[i].mul(ratio).div(this.POOL_TOKEN_BASE) ); + + // Update virtual balances + this.vTokenBalancesL2[i] = this.vTokenBalancesL2[i] + .mul(newTotalSupply) + .div(this.totalSupply); } } @@ -552,8 +713,6 @@ export class AmmPool { this.tokenBalancesL2[i].iadd(amount); logDebug("pool join: " + amount.toString(10)); } - join.actualMintAmount = mintAmount; - join.actualAmounts = amounts; // Mint await this.ctx.transfer( @@ -571,6 +730,9 @@ export class AmmPool { numTxs++; mintAmount = roundToFloatValue(mintAmount, Constants.Float24Encoding); poolTotal.iadd(mintAmount); + + join.actualMintAmount = mintAmount; + join.actualAmounts = amounts; } else if (transaction.txType === "Exit") { const exit = transaction; @@ -596,6 +758,14 @@ export class AmmPool { } if (valid) { + // Update virtual balances + assert(exit.burnAmount.lte(poolTotal), "burnAmount too big"); + const newTotalSupply = poolTotal.sub(exit.burnAmount); + for (let i = 0; i < this.tokens.length; i++) { + this.vTokenBalancesL2[i] = this.vTokenBalancesL2[i] + .mul(newTotalSupply) + .div(this.totalSupply); + } if (exit.authMethod !== AuthMethod.FORCE) { const storageID = exit.authMethod === AuthMethod.ECDSA || @@ -655,6 +825,56 @@ export class AmmPool { Constants.Float24Encoding ) ); + } else if (transaction.txType === "SetVirtualBalances") { + const poolVirtualBalances = transaction; + this.vTokenBalancesL2 = poolVirtualBalances.vBalances; + for (const vBalance of poolVirtualBalances.vBalances) { + logDebug("setVirtualBalance: " + vBalance.toString(10)); + } + } else if (transaction.txType === "Deposit") { + const deposit = transaction; + // Set virtual balances + for (let i = 0; i < this.tokens.length; i++) { + if (!deposit.amounts[i].isZero()) { + await this.ctx.requestDeposit( + owner, + this.tokens[i], + deposit.amounts[i] + ); + numTxs++; + } + } + } else if (transaction.txType === "Withdrawal") { + const withdrawal = transaction; + // Set virtual balances + for (let i = 0; i < this.tokens.length; i++) { + if (!withdrawal.amounts[i].isZero()) { + await this.ctx.requestWithdrawal( + owner, + this.tokens[i], + withdrawal.amounts[i], + Constants.zeroAddress, + new BN(0), + { + authMethod: AuthMethod.NONE + } + ); + numTxs++; + } + } + } + + if (doAmmUpdates) { + for (let i = 0; i < this.tokens.length; i++) { + await this.ctx.requestAmmUpdate( + owner, + this.tokens[i], + this.feeBips, + this.vTokenBalancesL2[i], + { authMethod: AuthMethod.NONE } + ); + numTxs++; + } } // Set the pool transaction data on the callback @@ -701,6 +921,35 @@ export class AmmPool { ); } + public static getPoolSetVirtualBalancesAuxData( + poolVirtualBalances: PoolVirtualBalances + ) { + const vBalances: string[] = []; + for (const vBalance of poolVirtualBalances.vBalances) { + vBalances.push(vBalance.toString(10)); + } + return web3.eth.abi.encodeParameter("tuple(uint96[],bytes)", [ + vBalances, + poolVirtualBalances.data + ]); + } + + public static getPoolDepositAuxData(deposit: PoolDeposit) { + const amounts: string[] = []; + for (const amount of deposit.amounts) { + amounts.push(amount.toString(10)); + } + return web3.eth.abi.encodeParameter("tuple(uint96[])", [amounts]); + } + + public static getPoolWithdrawalAuxData(withdrawal: PoolWithdrawal) { + const amounts: string[] = []; + for (const amount of withdrawal.amounts) { + amounts.push(amount.toString(10)); + } + return web3.eth.abi.encodeParameter("tuple(uint96[])", [amounts]); + } + public static getAuxiliaryData(transaction: TxType) { let poolTx: PoolTransaction; // Hack: fix json deserializing when the owner address is serialized as a decimal string @@ -713,12 +962,32 @@ export class AmmPool { data: this.getPoolJoinAuxData(transaction), signature: transaction.signature }; - } else { + } else if (transaction.txType === "Exit") { poolTx = { txType: PoolTransactionType.EXIT, data: this.getPoolExitAuxData(transaction), signature: transaction.signature }; + } else if (transaction.txType === "SetVirtualBalances") { + poolTx = { + txType: PoolTransactionType.SET_VIRTUAL_BALANCES, + data: this.getPoolSetVirtualBalancesAuxData(transaction), + signature: transaction.signature + }; + } else if (transaction.txType === "Deposit") { + poolTx = { + txType: PoolTransactionType.DEPOSIT, + data: this.getPoolDepositAuxData(transaction), + signature: transaction.signature + }; + } else if (transaction.txType === "Withdrawal") { + poolTx = { + txType: PoolTransactionType.WITHDRAW, + data: this.getPoolWithdrawalAuxData(transaction), + signature: transaction.signature + }; + } else { + assert(false); } //logDebug(poolTx); diff --git a/packages/loopring_v3/test/simulator.ts b/packages/loopring_v3/test/simulator.ts index 0b977e6e2..4bf20e3da 100644 --- a/packages/loopring_v3/test/simulator.ts +++ b/packages/loopring_v3/test/simulator.ts @@ -907,6 +907,15 @@ export class Simulator { .balance.iadd(s.fillBA) .isub(s.feeA); + // virtual balances + if ( + accountA.getBalance(tokenA).weightAMM.gt(new BN(0)) && + accountA.getBalance(tokenB).weightAMM.gt(new BN(0)) + ) { + accountA.getBalance(tokenA).weightAMM.isub(s.fillSA); + accountA.getBalance(tokenB).weightAMM.iadd(s.fillBA); + } + const tradeHistoryA = accountA.getBalance(tokenA).getStorage(storageIdA); tradeHistoryA.data = storageIdA > tradeHistoryA.storageID ? new BN(0) : tradeHistoryA.data; @@ -922,6 +931,15 @@ export class Simulator { .balance.iadd(s.fillBB) .isub(s.feeB); + // virtual balances + if ( + accountB.getBalance(tokenA).weightAMM.gt(new BN(0)) && + accountB.getBalance(tokenB).weightAMM.gt(new BN(0)) + ) { + accountB.getBalance(tokenB).weightAMM.isub(s.fillBA); + accountB.getBalance(tokenA).weightAMM.iadd(s.fillSA); + } + const tradeHistoryB = accountB.getBalance(tokenB).getStorage(storageIdB); tradeHistoryB.data = storageIdB > tradeHistoryB.storageID ? new BN(0) : tradeHistoryB.data; diff --git a/packages/loopring_v3/test/testAMMPool.ts b/packages/loopring_v3/test/testAMMPool.ts index e26a5e9e6..d9880f616 100644 --- a/packages/loopring_v3/test/testAMMPool.ts +++ b/packages/loopring_v3/test/testAMMPool.ts @@ -6,14 +6,14 @@ import { BalanceSnapshot, ExchangeTestUtil } from "./testExchangeUtil"; import { AuthMethod, SpotTrade } from "./types"; import { SignatureType, sign, verifySignature } from "../util/Signature"; -const AgentRegistry = artifacts.require("AgentRegistry"); - contract("LoopringAmmPool", (accounts: string[]) => { let ctx: ExchangeTestUtil; let sharedConfig: any; let agentRegistry: any; let registryOwner: string; + let ammController: any; + let assetManager: any; let ownerA: string; let ownerB: string; @@ -22,14 +22,35 @@ contract("LoopringAmmPool", (accounts: string[]) => { let amountsA: BN[]; let amountsB: BN[]; - const setupDefaultPool = async () => { + const getAmountOut = ( + amountIn: BN, + reserveIn: BN, + reserveOut: BN, + fee: number + ) => { + let amountInWithFee = amountIn.mul(new BN(10000 - fee)); + let numerator = amountInWithFee.mul(reserveOut); + let denominator = reserveIn.mul(new BN(10000)).add(amountInWithFee); + let amountOut = numerator.div(denominator); + return amountOut; + }; + + const setupDefaultPool = async ( + amplificationFactor = new BN(web3.utils.toWei("1", "ether")), + controller?: any, + assetManager?: any + ) => { + controller = controller ? controller : ammController; + const assetManagerAddress = assetManager + ? assetManager.address + : Constants.zeroAddress; + const feeBipsAMM = 30; const tokens = ["WETH", "GTO"]; const weights = [ - new BN(web3.utils.toWei("1", "ether")), - new BN(web3.utils.toWei("1", "ether")) + new BN(web3.utils.toWei("0", "ether")), + new BN(web3.utils.toWei("0", "ether")) ]; - for (const owner of [ownerA, ownerB]) { for (const token of tokens) { await ctx.deposit( @@ -42,7 +63,22 @@ contract("LoopringAmmPool", (accounts: string[]) => { } const pool = new AmmPool(ctx); - await pool.setupPool(sharedConfig, tokens, weights, feeBipsAMM); + await pool.setupPool( + sharedConfig, + tokens, + weights, + feeBipsAMM, + amplificationFactor, + controller.address, + assetManagerAddress + ); + + if (controller) { + await controller.setAmplificationFactor( + pool.contract.address, + amplificationFactor + ); + } await agentRegistry.registerUniversalAgent(pool.contract.address, true, { from: registryOwner @@ -76,6 +112,12 @@ contract("LoopringAmmPool", (accounts: string[]) => { await sharedConfig.setMaxForcedExitAge(3600 * 24 * 7); await sharedConfig.setMaxForcedExitCount(100); await sharedConfig.setForcedExitFee(web3.utils.toWei("0.001", "ether")); + + const amplifiedAmmController = artifacts.require("AmplifiedAmmController"); + ammController = await amplifiedAmmController.new(); + + const TestAssetManager = artifacts.require("TestAssetManager"); + assetManager = await TestAssetManager.new(); }); after(async () => { @@ -91,6 +133,7 @@ contract("LoopringAmmPool", (accounts: string[]) => { // Create the agent registry registryOwner = accounts[7]; + const AgentRegistry = artifacts.require("AgentRegistry"); agentRegistry = await AgentRegistry.new({ from: registryOwner }); // Register it on the exchange contract @@ -517,6 +560,244 @@ contract("LoopringAmmPool", (accounts: string[]) => { await pool.verifySupply(); }); + it("Set virtual balances", async () => { + const pool = await setupDefaultPool(); + + await pool.prePoolTransactions(); + await pool.join( + ownerA, + pool.POOL_TOKEN_BASE, + [ + new BN(web3.utils.toWei("10000", "ether")), + new BN(web3.utils.toWei("20000", "ether")) + ], + { authMethod: AuthMethod.ECDSA } + ); + await pool.join( + ownerB, + pool.POOL_TOKEN_BASE.div(new BN(11)), + [ + new BN(web3.utils.toWei("1000", "ether")), + new BN(web3.utils.toWei("2000", "ether")) + ], + { + authMethod: AuthMethod.EDDSA, + fee: new BN(web3.utils.toWei("100", "ether")) + } + ); + await ctx.submitTransactions(16); + + await pool.prePoolTransactions(); + + // Set virtual balances to the actual balances + const balances = await pool.getBalancesL2(); + await pool.setVirtualBalances(balances); + + await ctx.submitTransactions(16); + await ctx.submitPendingBlocks(); + await pool.verifySupply(); + }); + + it("Manage pool assets", async () => { + const pool = await setupDefaultPool( + new BN(web3.utils.toWei("1", "ether")), + undefined, + assetManager + ); + + const amounts = [ + new BN(web3.utils.toWei("10000", "ether")), + new BN(web3.utils.toWei("20000", "ether")) + ]; + + await pool.prePoolTransactions(); + await pool.join(ownerA, pool.POOL_TOKEN_BASE, amounts, { + authMethod: AuthMethod.ECDSA + }); + await ctx.submitTransactions(16); + + await pool.prePoolTransactions(); + await pool.withdraw(amounts); + await ctx.submitTransactions(16); + + await ctx.submitPendingBlocks(); + + await pool.prePoolTransactions(); + await pool.deposit(amounts); + await ctx.submitTransactions(16); + + await ctx.submitPendingBlocks(); + await pool.verifySupply(); + }); + + it("Amplified pool", async () => { + const pool = await setupDefaultPool( + new BN(web3.utils.toWei("2", "ether")) + ); + + // Join + await pool.prePoolTransactions(); + const joinA = await pool.join( + ownerA, + pool.POOL_TOKEN_BASE, + [ + new BN(web3.utils.toWei("10000", "ether")), + new BN(web3.utils.toWei("20000", "ether")) + ], + { authMethod: AuthMethod.ECDSA } + ); + const joinB = await pool.join( + ownerB, + pool.POOL_TOKEN_BASE.div(new BN(11)), + [ + new BN(web3.utils.toWei("1000", "ether")), + new BN(web3.utils.toWei("2000", "ether")) + ], + { + authMethod: AuthMethod.EDDSA, + fee: new BN(web3.utils.toWei("100", "ether")) + } + ); + await ctx.submitTransactions(16); + + // Swap + { + const vBalances = await pool.getVirtualBalancesL2(); + const amountY = new BN(web3.utils.toWei("9888", "ether")); + const amountX = getAmountOut( + amountY, + vBalances[1], + vBalances[0], + pool.feeBips + ); + + const ring: SpotTrade = { + orderA: { + owner: pool.contract.address, + tokenS: "WETH", + tokenB: "GTO", + amountS: amountX, + amountB: amountY, + feeBips: 0, + amm: true + }, + orderB: { + tokenS: "GTO", + tokenB: "WETH", + amountS: amountY, + amountB: amountX + }, + expected: { + orderA: { filledFraction: 1.0, spread: new BN(0) }, + orderB: { filledFraction: 1.0 } + } + }; + await ctx.setupRing(ring, true, true, false, true); + await ctx.sendRing(ring); + await ctx.submitTransactions(16); + await ctx.submitPendingBlocks(); + } + + // Join + await pool.prePoolTransactions(); + const joinC = await pool.join( + ownerB, + pool.POOL_TOKEN_BASE.div(new BN(12)), + [ + new BN(web3.utils.toWei("633", "ether")), + new BN(web3.utils.toWei("2899", "ether")) + ], + { authMethod: AuthMethod.ECDSA } + ); + await ctx.submitTransactions(16); + await ctx.submitPendingBlocks(); + + // Swap + { + const vBalances = await pool.getVirtualBalancesL2(); + const amountY = new BN(web3.utils.toWei("4400", "ether")); + const amountX = getAmountOut( + amountY, + vBalances[0], + vBalances[1], + pool.feeBips + ); + + // Small bias + amountY.iadd(new BN(web3.utils.toWei("1", "ether"))); + + const ring: SpotTrade = { + orderA: { + owner: pool.contract.address, + tokenS: "GTO", + tokenB: "WETH", + amountS: amountX, + amountB: amountY, + feeBips: 0, + amm: true + }, + orderB: { + tokenS: "WETH", + tokenB: "GTO", + amountS: amountY, + amountB: amountX + }, + expected: { + orderA: { filledFraction: 1.0, spread: new BN(0) }, + orderB: { filledFraction: 1.0 } + } + }; + await ctx.setupRing(ring, true, true, false, true); + await ctx.sendRing(ring); + await ctx.submitTransactions(16); + await ctx.submitPendingBlocks(); + } + + // Exit + await pool.prePoolTransactions(); + const tokenBalances = await pool.getBalancesL2(); + const totalSupply = new BN(pool.totalSupply.toString(10)); + await pool.exit( + ownerA, + joinA.actualMintAmount, + [ + joinA.actualMintAmount + .mul(tokenBalances[0]) + .mul(new BN(9999)) + .div(totalSupply.mul(new BN(10000))), + joinA.actualMintAmount + .mul(tokenBalances[1]) + .mul(new BN(9999)) + .div(totalSupply.mul(new BN(10000))) + ], + { + authMethod: AuthMethod.EDDSA, + fee: new BN(web3.utils.toWei("100", "ether")) + } + ); + const totalMintAmountB = joinB.actualMintAmount.add( + joinC.actualMintAmount + ); + await pool.exit( + ownerB, + totalMintAmountB, + [ + totalMintAmountB + .mul(tokenBalances[0]) + .mul(new BN(9999)) + .div(totalSupply.mul(new BN(10000))), + totalMintAmountB + .mul(tokenBalances[1]) + .mul(new BN(9999)) + .div(totalSupply.mul(new BN(10000))) + ], + { authMethod: AuthMethod.ECDSA } + ); + await ctx.submitTransactions(16); + await ctx.submitPendingBlocks(); + await pool.verifySupply(new BN(0)); + }); + it("No join signature", async () => { const pool = await setupDefaultPool(); await pool.prePoolTransactions(); @@ -800,9 +1081,7 @@ contract("LoopringAmmPool", (accounts: string[]) => { "INVALID_CHALLENGE" ); - const maxForcedExitAge = ( - await sharedConfig.maxForcedExitAge() - ).toNumber(); + const maxForcedExitAge = (await sharedConfig.maxForcedExitAge()).toNumber(); // Wait await ctx.advanceBlockTimestamp(maxForcedExitAge - 100); diff --git a/packages/loopring_v3/test/testExchangeAMM.ts b/packages/loopring_v3/test/testExchangeAMM.ts index d63b7581c..d1ca5ea90 100644 --- a/packages/loopring_v3/test/testExchangeAMM.ts +++ b/packages/loopring_v3/test/testExchangeAMM.ts @@ -130,6 +130,60 @@ contract("Exchange", (accounts: string[]) => { await exchangeTestUtil.submitPendingBlocks(); }); + it("Successful swap (AMM maker)", async () => { + const ring: SpotTrade = { + orderA: { + owner: exchangeTestUtil.testContext.orderOwners[0], + tokenS: "WETH", + tokenB: "GTO", + amountS: new BN(web3.utils.toWei("8.1", "ether")), + amountB: new BN(web3.utils.toWei("10", "ether")), + balanceS: new BN(web3.utils.toWei("100", "ether")), + balanceB: new BN(web3.utils.toWei("100", "ether")), + feeBips: 0, + amm: true + }, + orderB: { + tokenS: "GTO", + tokenB: "WETH", + amountS: new BN(web3.utils.toWei("10", "ether")), + amountB: new BN(web3.utils.toWei("8.1", "ether")) + }, + expected: { + orderA: { filledFraction: 1.0, spread: new BN(0) }, + orderB: { filledFraction: 1.0 } + } + }; + await exchangeTestUtil.setupRing(ring); + + await exchangeTestUtil.deposit( + exchangeTestUtil.exchangeOperator, + exchangeTestUtil.exchangeOperator, + ring.orderA.tokenB, + ring.orderA.amountB + ); + + const feeBipsAMM = 30; + const tokenWeightS = ring.orderA.balanceS; + const tokenWeightB = ring.orderA.balanceB; + await exchangeTestUtil.requestAmmUpdate( + ring.orderA.owner, + ring.orderA.tokenS, + feeBipsAMM, + tokenWeightS + ); + await exchangeTestUtil.requestAmmUpdate( + ring.orderA.owner, + ring.orderA.tokenB, + feeBipsAMM, + tokenWeightB + ); + + await exchangeTestUtil.sendRing(ring); + await exchangeTestUtil.submitTransactions(); + await exchangeTestUtil.submitPendingBlocks(); + }); + it("Successful swap (AMM maker)", async () => { const ring: SpotTrade = { orderA: { @@ -164,8 +218,8 @@ contract("Exchange", (accounts: string[]) => { ); const feeBipsAMM = 30; - const tokenWeightS = new BN(web3.utils.toWei("1", "ether")); - const tokenWeightB = new BN(web3.utils.toWei("1", "ether")); + const tokenWeightS = ring.orderA.balanceS; + const tokenWeightB = ring.orderA.balanceB; await exchangeTestUtil.requestAmmUpdate( ring.orderA.owner, ring.orderA.tokenS, @@ -187,7 +241,7 @@ contract("Exchange", (accounts: string[]) => { it("Successful swap (AMM taker)", async () => { const ring: SpotTrade = { orderB: { - owner: exchangeTestUtil.testContext.orderOwners[0], + owner: exchangeTestUtil.testContext.orderOwners[4], tokenS: "WETH", tokenB: "GTO", amountS: new BN(web3.utils.toWei("98", "ether")), @@ -218,8 +272,8 @@ contract("Exchange", (accounts: string[]) => { ); const feeBipsAMM = 30; - const tokenWeightS = new BN(web3.utils.toWei("1", "ether")); - const tokenWeightB = new BN(web3.utils.toWei("1", "ether")); + const tokenWeightS = ring.orderB.balanceS; + const tokenWeightB = ring.orderB.balanceB; await exchangeTestUtil.requestAmmUpdate( ring.orderB.owner, ring.orderB.tokenS, @@ -283,31 +337,29 @@ contract("Exchange", (accounts: string[]) => { ); const feeBipsAMM = 30; - const tokenWeightS = new BN(web3.utils.toWei("1", "ether")); - const tokenWeightB = new BN(web3.utils.toWei("1", "ether")); await exchangeTestUtil.requestAmmUpdate( ring.orderA.owner, ring.orderA.tokenS, feeBipsAMM, - tokenWeightS + ring.orderA.balanceS ); await exchangeTestUtil.requestAmmUpdate( ring.orderA.owner, ring.orderA.tokenB, feeBipsAMM, - tokenWeightB + ring.orderA.balanceB ); await exchangeTestUtil.requestAmmUpdate( ring.orderB.owner, ring.orderB.tokenS, feeBipsAMM, - tokenWeightS + ring.orderB.balanceS ); await exchangeTestUtil.requestAmmUpdate( ring.orderB.owner, ring.orderB.tokenB, feeBipsAMM, - tokenWeightB + ring.orderB.balanceB ); await exchangeTestUtil.sendRing(ring); @@ -349,8 +401,8 @@ contract("Exchange", (accounts: string[]) => { ); const feeBipsAMM = 30; - const tokenWeightS = new BN(web3.utils.toWei("1", "ether")); - const tokenWeightB = new BN(web3.utils.toWei("1", "ether")); + const tokenWeightS = ring.orderA.balanceS; + const tokenWeightB = ring.orderA.balanceB; await exchangeTestUtil.requestAmmUpdate( ring.orderA.owner, ring.orderA.tokenS, @@ -403,8 +455,8 @@ contract("Exchange", (accounts: string[]) => { ); const feeBipsAMM = 30; - const tokenWeightS = new BN(web3.utils.toWei("1", "ether")); - const tokenWeightB = new BN(web3.utils.toWei("1", "ether")); + const tokenWeightS = ring.orderA.balanceS; + const tokenWeightB = ring.orderA.balanceB; await exchangeTestUtil.requestAmmUpdate( ring.orderA.owner, ring.orderA.tokenS, @@ -458,8 +510,8 @@ contract("Exchange", (accounts: string[]) => { ); const feeBipsAMM = 30; - const tokenWeightS = new BN(web3.utils.toWei("1", "ether")); - const tokenWeightB = new BN(web3.utils.toWei("1", "ether")); + const tokenWeightS = ring.orderA.balanceS; + const tokenWeightB = ring.orderA.balanceB; await exchangeTestUtil.requestAmmUpdate( ring.orderA.owner, ring.orderA.tokenS, @@ -512,7 +564,7 @@ contract("Exchange", (accounts: string[]) => { // Only set the weight of a single token const feeBipsAMM = 0; - const tokenWeightS = new BN(web3.utils.toWei("1", "ether")); + const tokenWeightS = ring.orderA.balanceS; await exchangeTestUtil.requestAmmUpdate( ring.orderA.owner, ring.orderA.tokenS, diff --git a/packages/loopring_v3/test/testExchangeUtil.ts b/packages/loopring_v3/test/testExchangeUtil.ts index b04b6eddb..adfc92bf8 100644 --- a/packages/loopring_v3/test/testExchangeUtil.ts +++ b/packages/loopring_v3/test/testExchangeUtil.ts @@ -1867,11 +1867,13 @@ export class ExchangeTestUtil { interface TransactionReceiverCallbacks { callbacks: OnchainBlockCallback[]; receivers: string[]; + beforeBlockSubmission: boolean; } const transactionReceiverCallbacks: TransactionReceiverCallbacks = { callbacks: [], - receivers: [] + receivers: [], + beforeBlockSubmission: true }; //console.log("Block callbacks: "); @@ -3069,6 +3071,21 @@ export class ExchangeTestUtil { } } + public async getOffchainVirtualBalance(owner: string, token: string) { + const accountID = this.getAccountID(owner); + const tokenID = this.getTokenIdFromNameOrAddress(token); + const latestBlockIdx = this.blocks[this.exchangeId].length - 1; + const state = await Simulator.loadExchangeState( + this.exchangeId, + latestBlockIdx + ); + try { + return state.accounts[accountID].balances[tokenID].weightAMM; + } catch { + return new BN(0); + } + } + public async getTokenContract(token: string) { if (!token.startsWith("0x")) { token = this.testContext.tokenSymbolAddrMap.get(token);