Compare commits

..

3 Commits

Author SHA1 Message Date
tequ
ec65e622aa Merge fixAMMv1_3 amendment into featureAMM amendment 2026-02-25 16:20:43 +10:00
Gregory Tsipenyuk
65837f49e1 fix: Add AMMv1_3 amendment (#5203)
* Add AMM bid/create/deposit/swap/withdraw/vote invariants:
  - Deposit, Withdrawal invariants: `sqrt(asset1Balance * asset2Balance) >= LPTokens`.
  - Bid: `sqrt(asset1Balance * asset2Balance) > LPTokens` and the pool balances don't change.
  - Create: `sqrt(asset1Balance * assetBalance2) == LPTokens`.
  - Swap: `asset1BalanceAfter * asset2BalanceAfter >= asset1BalanceBefore * asset2BalanceBefore`
     and `LPTokens` don't change.
  - Vote: `LPTokens` and pool balances don't change.
  - All AMM and swap transactions: amounts and tokens are greater than zero, except on withdrawal if all tokens
    are withdrawn.
* Add AMM deposit and withdraw rounding to ensure AMM invariant:
  - On deposit, tokens out are rounded downward and deposit amount is rounded upward.
  - On withdrawal, tokens in are rounded upward and withdrawal amount is rounded downward.
* Add Order Book Offer invariant to verify consumed amounts. Consumed amounts are less than the offer.
* Fix Bid validation. `AuthAccount` can't have duplicate accounts or the submitter account.
2026-02-25 16:20:43 +10:00
RichardAH
e5b21f026e Merge pull request #692 from Xahau/sync-2.4.0-rebased
Sync: Ripple(d) 2.4.0
2026-02-24 16:07:09 +10:00
20 changed files with 1989 additions and 552 deletions

View File

@@ -97,6 +97,12 @@ public:
static IOUAmount
minPositiveAmount();
friend std::ostream&
operator<<(std::ostream& os, IOUAmount const& x)
{
return os << to_string(x);
}
};
inline IOUAmount::IOUAmount(beast::Zero)

View File

@@ -28,6 +28,9 @@
namespace ripple {
bool
isFeatureEnabled(uint256 const& feature);
class DigestAwareReadView;
/** Rules controlling protocol behavior. */

View File

@@ -153,4 +153,12 @@ Rules::operator!=(Rules const& other) const
{
return !(*this == other);
}
bool
isFeatureEnabled(uint256 const& feature)
{
auto const& rules = getCurrentTransactionRules();
return rules && rules->enabled(feature);
}
} // namespace ripple

View File

@@ -582,7 +582,7 @@ class AMMClawback_test : public jtx::AMMTest
env.close();
BEAST_EXPECT(amm.expectBalances(
USD(4000), EUR(5000), IOUAmount{4472135954999580, -12}));
USD(4000), EUR(5000), IOUAmount{4472135954999579, -12}));
// gw clawback 1000 USD from the AMM pool
env(amm::ammClawback(gw, alice, USD, EUR, USD(1000)),
@@ -602,11 +602,11 @@ class AMMClawback_test : public jtx::AMMTest
// 1000 USD and 1250 EUR was withdrawn from the AMM pool, so the
// current balance is 3000 USD and 3750 EUR.
BEAST_EXPECT(amm.expectBalances(
USD(3000), EUR(3750), IOUAmount{3354101966249685, -12}));
USD(3000), EUR(3750), IOUAmount{3354101966249684, -12}));
// Alice has 3/4 of its initial lptokens Left.
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{3354101966249685, -12}));
amm.expectLPTokens(alice, IOUAmount{3354101966249684, -12}));
// gw clawback another 500 USD from the AMM pool.
env(amm::ammClawback(gw, alice, USD, EUR, USD(500)),
@@ -618,13 +618,9 @@ class AMMClawback_test : public jtx::AMMTest
env.require(balance(alice, gw["USD"](2000)));
BEAST_EXPECT(amm.expectBalances(
STAmount{USD, UINT64_C(2500000000000001), -12},
STAmount{EUR, UINT64_C(3125000000000001), -12},
IOUAmount{2795084971874738, -12}));
USD(2500), EUR(3125), IOUAmount{2795084971874737, -12}));
BEAST_EXPECT(
env.balance(alice, EUR) ==
STAmount(EUR, UINT64_C(2874999999999999), -12));
BEAST_EXPECT(env.balance(alice, EUR) == EUR(2875));
// gw clawback small amount, 1 USD.
env(amm::ammClawback(gw, alice, USD, EUR, USD(1)), ter(tesSUCCESS));
@@ -634,13 +630,9 @@ class AMMClawback_test : public jtx::AMMTest
env.require(balance(alice, gw["USD"](2000)));
BEAST_EXPECT(amm.expectBalances(
STAmount{USD, UINT64_C(2499000000000002), -12},
STAmount{EUR, UINT64_C(3123750000000002), -12},
IOUAmount{2793966937885989, -12}));
USD(2499), EUR(3123.75), IOUAmount{2793966937885987, -12}));
BEAST_EXPECT(
env.balance(alice, EUR) ==
STAmount(EUR, UINT64_C(2876249999999998), -12));
BEAST_EXPECT(env.balance(alice, EUR) == EUR(2876.25));
// gw clawback 4000 USD, exceeding the current balance. We
// will clawback all.
@@ -714,13 +706,13 @@ class AMMClawback_test : public jtx::AMMTest
// gw2 creates AMM pool of XRP/EUR, alice and bob deposit XRP/EUR.
AMM amm2(env, gw2, XRP(3000), EUR(1000), ter(tesSUCCESS));
BEAST_EXPECT(amm2.expectBalances(
EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9}));
EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9}));
amm2.deposit(alice, EUR(1000), XRP(3000));
BEAST_EXPECT(amm2.expectBalances(
EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9}));
EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9}));
amm2.deposit(bob, EUR(1000), XRP(3000));
BEAST_EXPECT(amm2.expectBalances(
EUR(3000), XRP(9000), IOUAmount{5196152422706634, -9}));
EUR(3000), XRP(9000), IOUAmount{5196152422706631, -9}));
env.close();
auto aliceXrpBalance = env.balance(alice, XRP);
@@ -744,9 +736,9 @@ class AMMClawback_test : public jtx::AMMTest
expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(1000)));
BEAST_EXPECT(amm.expectBalances(
USD(2500), XRP(5000), IOUAmount{3535533905932738, -9}));
USD(2500), XRP(5000), IOUAmount{3535533905932737, -9}));
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10}));
amm.expectLPTokens(alice, IOUAmount{7071067811865474, -10}));
BEAST_EXPECT(
amm.expectLPTokens(bob, IOUAmount{1414213562373095, -9}));
@@ -761,13 +753,11 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(
expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(20)));
BEAST_EXPECT(amm.expectBalances(
STAmount{USD, UINT64_C(2490000000000001), -12},
XRP(4980),
IOUAmount{3521391770309008, -9}));
USD(2'490), XRP(4980), IOUAmount{3521391770309006, -9}));
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{7071067811865480, -10}));
amm.expectLPTokens(alice, IOUAmount{7071067811865474, -10}));
BEAST_EXPECT(
amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9}));
amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9}));
// gw2 clawback 200 EUR from amm2.
env(amm::ammClawback(gw2, alice, EUR, XRP, EUR(200)),
@@ -781,11 +771,11 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(expectLedgerEntryRoot(
env, alice, aliceXrpBalance + XRP(1000) + XRP(600)));
BEAST_EXPECT(amm2.expectBalances(
EUR(2800), XRP(8400), IOUAmount{4849742261192859, -9}));
EUR(2800), XRP(8400), IOUAmount{4849742261192856, -9}));
BEAST_EXPECT(
amm2.expectLPTokens(alice, IOUAmount{1385640646055103, -9}));
amm2.expectLPTokens(alice, IOUAmount{1385640646055102, -9}));
BEAST_EXPECT(
amm2.expectLPTokens(bob, IOUAmount{1732050807568878, -9}));
amm2.expectLPTokens(bob, IOUAmount{1732050807568877, -9}));
// gw claw back 1000 USD from alice in amm, which exceeds alice's
// balance. This will clawback all the remaining LP tokens of alice
@@ -801,14 +791,15 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(expectLedgerEntryRoot(
env,
alice,
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000)));
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) -
XRPAmount{1}));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
BEAST_EXPECT(
amm.expectLPTokens(bob, IOUAmount{1400071426749365, -9}));
amm.expectLPTokens(bob, IOUAmount{1400071426749364, -9}));
BEAST_EXPECT(amm.expectBalances(
STAmount{USD, UINT64_C(1990000000000001), -12},
XRP(3980),
IOUAmount{2814284989122460, -9}));
USD(1'990),
XRPAmount{3'980'000'001},
IOUAmount{2814284989122459, -9}));
// gw clawback 1000 USD from bob in amm, which also exceeds bob's
// balance in amm. All bob's lptoken in amm will be consumed, which
@@ -823,7 +814,8 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(expectLedgerEntryRoot(
env,
alice,
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000)));
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) -
XRPAmount{1}));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, bobXrpBalance + XRP(20) + XRP(1980)));
@@ -846,8 +838,8 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(expectLedgerEntryRoot(
env,
alice,
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) +
XRP(2400)));
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + XRP(2400) -
XRPAmount{1}));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, bobXrpBalance + XRP(20) + XRP(1980)));
@@ -855,9 +847,9 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(amm2.expectLPTokens(alice, IOUAmount(0)));
BEAST_EXPECT(amm2.expectBalances(
EUR(2000), XRP(6000), IOUAmount{3464101615137756, -9}));
EUR(2000), XRP(6000), IOUAmount{3464101615137754, -9}));
// gw2 claw back 2000 EUR from bib in amm2, which exceeds bob's
// gw2 claw back 2000 EUR from bob in amm2, which exceeds bob's
// balance. All bob's lptokens will be consumed, which corresponds
// to 1000EUR / 3000 XRP.
env(amm::ammClawback(gw2, bob, EUR, XRP, EUR(2000)),
@@ -872,8 +864,8 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(expectLedgerEntryRoot(
env,
alice,
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) +
XRP(2400)));
aliceXrpBalance + XRP(1000) + XRP(600) + XRP(1000) + XRP(2400) -
XRPAmount{1}));
BEAST_EXPECT(expectLedgerEntryRoot(
env, bob, bobXrpBalance + XRP(20) + XRP(1980) + XRP(3000)));
@@ -882,7 +874,7 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(amm2.expectLPTokens(bob, IOUAmount(0)));
BEAST_EXPECT(amm2.expectBalances(
EUR(1000), XRP(3000), IOUAmount{1732050807568878, -9}));
EUR(1000), XRP(3000), IOUAmount{1732050807568877, -9}));
}
}
@@ -941,20 +933,20 @@ class AMMClawback_test : public jtx::AMMTest
env.close();
BEAST_EXPECT(amm.expectBalances(
USD(4000), EUR(5000), IOUAmount{4472135954999580, -12}));
USD(4000), EUR(5000), IOUAmount{4472135954999579, -12}));
amm.deposit(bob, USD(2000), EUR(2500));
BEAST_EXPECT(amm.expectBalances(
USD(6000), EUR(7500), IOUAmount{6708203932499370, -12}));
USD(6000), EUR(7500), IOUAmount{6708203932499368, -12}));
amm.deposit(carol, USD(1000), EUR(1250));
BEAST_EXPECT(amm.expectBalances(
USD(7000), EUR(8750), IOUAmount{7826237921249265, -12}));
USD(7000), EUR(8750), IOUAmount{7826237921249262, -12}));
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12}));
amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12}));
BEAST_EXPECT(
amm.expectLPTokens(bob, IOUAmount{2236067977499790, -12}));
amm.expectLPTokens(bob, IOUAmount{2236067977499789, -12}));
BEAST_EXPECT(
amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12}));
amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12}));
env.require(balance(alice, gw["USD"](2000)));
env.require(balance(alice, gw2["EUR"](1000)));
@@ -969,15 +961,15 @@ class AMMClawback_test : public jtx::AMMTest
env.close();
BEAST_EXPECT(amm.expectBalances(
STAmount{USD, UINT64_C(4999999999999999), -12},
STAmount{EUR, UINT64_C(6249999999999999), -12},
IOUAmount{5590169943749475, -12}));
STAmount{USD, UINT64_C(5000000000000001), -12},
STAmount{EUR, UINT64_C(6250000000000001), -12},
IOUAmount{5590169943749473, -12}));
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12}));
amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12}));
BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0)));
BEAST_EXPECT(
amm.expectLPTokens(carol, IOUAmount{1118033988749895, -12}));
amm.expectLPTokens(carol, IOUAmount{1118033988749894, -12}));
// Bob will get 2500 EUR back.
env.require(balance(alice, gw["USD"](2000)));
@@ -988,7 +980,7 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(
env.balance(bob, EUR) ==
STAmount(EUR, UINT64_C(5000000000000001), -12));
STAmount(EUR, UINT64_C(4999999999999999), -12));
env.require(balance(carol, gw["USD"](3000)));
env.require(balance(carol, gw2["EUR"](2750)));
@@ -997,12 +989,12 @@ class AMMClawback_test : public jtx::AMMTest
ter(tesSUCCESS));
env.close();
BEAST_EXPECT(amm.expectBalances(
STAmount{USD, UINT64_C(3999999999999999), -12},
STAmount{EUR, UINT64_C(4999999999999999), -12},
IOUAmount{4472135954999580, -12}));
STAmount{USD, UINT64_C(4000000000000001), -12},
STAmount{EUR, UINT64_C(5000000000000002), -12},
IOUAmount{4472135954999579, -12}));
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{4472135954999580, -12}));
amm.expectLPTokens(alice, IOUAmount{4472135954999579, -12}));
BEAST_EXPECT(amm.expectLPTokens(bob, IOUAmount(0)));
BEAST_EXPECT(amm.expectLPTokens(carol, IOUAmount(0)));
@@ -1042,13 +1034,13 @@ class AMMClawback_test : public jtx::AMMTest
// gw creates AMM pool of XRP/USD, alice and bob deposit XRP/USD.
AMM amm(env, gw, XRP(2000), USD(10000), ter(tesSUCCESS));
BEAST_EXPECT(amm.expectBalances(
USD(10000), XRP(2000), IOUAmount{4472135954999580, -9}));
USD(10000), XRP(2000), IOUAmount{4472135954999579, -9}));
amm.deposit(alice, USD(1000), XRP(200));
BEAST_EXPECT(amm.expectBalances(
USD(11000), XRP(2200), IOUAmount{4919349550499538, -9}));
USD(11000), XRP(2200), IOUAmount{4919349550499536, -9}));
amm.deposit(bob, USD(2000), XRP(400));
BEAST_EXPECT(amm.expectBalances(
USD(13000), XRP(2600), IOUAmount{5813776741499453, -9}));
USD(13000), XRP(2600), IOUAmount{5813776741499451, -9}));
env.close();
auto aliceXrpBalance = env.balance(alice, XRP);
@@ -1059,9 +1051,11 @@ class AMMClawback_test : public jtx::AMMTest
ter(tesSUCCESS));
env.close();
BEAST_EXPECT(amm.expectBalances(
USD(12000), XRP(2400), IOUAmount{5366563145999495, -9}));
BEAST_EXPECT(
expectLedgerEntryRoot(env, alice, aliceXrpBalance + XRP(200)));
USD(12000),
XRPAmount(2400000001),
IOUAmount{5366563145999494, -9}));
BEAST_EXPECT(expectLedgerEntryRoot(
env, alice, aliceXrpBalance + XRP(200) - XRPAmount{1}));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
// gw clawback all bob's USD in amm. (2000 USD / 400 XRP)
@@ -1069,7 +1063,9 @@ class AMMClawback_test : public jtx::AMMTest
ter(tesSUCCESS));
env.close();
BEAST_EXPECT(amm.expectBalances(
USD(10000), XRP(2000), IOUAmount{4472135954999580, -9}));
USD(10000),
XRPAmount(2000000001),
IOUAmount{4472135954999579, -9}));
BEAST_EXPECT(
expectLedgerEntryRoot(env, bob, bobXrpBalance + XRP(400)));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
@@ -1125,10 +1121,9 @@ class AMMClawback_test : public jtx::AMMTest
amm.deposit(bob, USD(4000), EUR(1000));
BEAST_EXPECT(
amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000)));
amm.deposit(carol, USD(2000), EUR(500));
amm.deposit(carol, USD(2000.25), EUR(500));
BEAST_EXPECT(
amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000)));
// gw clawback 1000 USD from carol.
env(amm::ammClawback(gw, carol, USD, EUR, USD(1000)), ter(tesSUCCESS));
env.close();
@@ -1142,7 +1137,9 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(bob, USD) == USD(5000));
BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(carol, USD) == USD(6000));
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(5999'999999999999), -12));
// 250 EUR goes back to carol.
BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750));
@@ -1164,7 +1161,9 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(env.balance(bob, USD) == USD(5000));
// 250 EUR did not go back to bob because tfClawTwoAssets is set.
BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(carol, USD) == USD(6000));
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(5999'999999999999), -12));
BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750));
// gw clawback all USD from alice and set tfClawTwoAssets.
@@ -1181,7 +1180,9 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(bob, USD) == USD(5000));
BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(carol, USD) == USD(6000));
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(5999'999999999999), -12));
BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750));
}
@@ -1367,11 +1368,9 @@ class AMMClawback_test : public jtx::AMMTest
env(amm::ammClawback(gw2, gw, EUR, USD, EUR(1000)), ter(tesSUCCESS));
env.close();
BEAST_EXPECT(amm.expectBalances(
USD(4500),
STAmount(EUR, UINT64_C(9000000000000001), -12),
IOUAmount{6363961030678928, -12}));
USD(4500), EUR(9000), IOUAmount{6363961030678928, -12}));
BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13}));
BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13}));
BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12}));
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{4242640687119285, -12}));
@@ -1385,11 +1384,9 @@ class AMMClawback_test : public jtx::AMMTest
env(amm::ammClawback(gw2, alice, EUR, USD, EUR(4000)), ter(tesSUCCESS));
env.close();
BEAST_EXPECT(amm.expectBalances(
USD(2500),
STAmount(EUR, UINT64_C(5000000000000001), -12),
IOUAmount{3535533905932738, -12}));
USD(2500), EUR(5000), IOUAmount{3535533905932738, -12}));
BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865480, -13}));
BEAST_EXPECT(amm.expectLPTokens(gw, IOUAmount{7071067811865475, -13}));
BEAST_EXPECT(amm.expectLPTokens(gw2, IOUAmount{1414213562373095, -12}));
BEAST_EXPECT(
amm.expectLPTokens(alice, IOUAmount{1414213562373095, -12}));
@@ -1653,7 +1650,7 @@ class AMMClawback_test : public jtx::AMMTest
amm.deposit(bob, USD(4000), EUR(1000));
BEAST_EXPECT(
amm.expectBalances(USD(12000), EUR(3000), IOUAmount(6000)));
amm.deposit(carol, USD(2000), EUR(500));
amm.deposit(carol, USD(2000.25), EUR(500));
BEAST_EXPECT(
amm.expectBalances(USD(14000), EUR(3500), IOUAmount(7000)));
@@ -1675,7 +1672,9 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(bob, USD) == USD(5000));
BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(carol, USD) == USD(6000));
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(5999'999999999999), -12));
// 250 EUR goes back to carol.
BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750));
@@ -1697,7 +1696,9 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(env.balance(bob, USD) == USD(5000));
// 250 EUR did not go back to bob because tfClawTwoAssets is set.
BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(carol, USD) == USD(6000));
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(5999'999999999999), -12));
BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750));
// gw clawback all USD from alice and set tfClawTwoAssets.
@@ -1715,7 +1716,9 @@ class AMMClawback_test : public jtx::AMMTest
BEAST_EXPECT(env.balance(alice, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(bob, USD) == USD(5000));
BEAST_EXPECT(env.balance(bob, EUR) == EUR(8000));
BEAST_EXPECT(env.balance(carol, USD) == USD(6000));
BEAST_EXPECT(
env.balance(carol, USD) ==
STAmount(USD, UINT64_C(5999'999999999999), -12));
BEAST_EXPECT(env.balance(carol, EUR) == EUR(7750));
}
}
@@ -1764,12 +1767,12 @@ class AMMClawback_test : public jtx::AMMTest
env.close();
BEAST_EXPECT(amm.expectBalances(
STAmount(USD, UINT64_C(5656854249492380), -13),
XRP(70.710678),
STAmount(USD, UINT64_C(565'685424949238), -12),
XRP(70.710679),
IOUAmount(200000)));
BEAST_EXPECT(amm.expectLPTokens(alice, IOUAmount(0)));
BEAST_EXPECT(expectLedgerEntryRoot(
env, alice, aliceXrpBalance + XRP(29.289322)));
env, alice, aliceXrpBalance + XRP(29.289321)));
}
void

File diff suppressed because it is too large Load Diff

View File

@@ -125,7 +125,6 @@ class AMM
STAmount const asset1_;
STAmount const asset2_;
uint256 const ammID_;
IOUAmount const initialLPTokens_;
bool log_;
bool doClose_;
// Predict next purchase price
@@ -138,6 +137,7 @@ class AMM
std::uint32_t const fee_;
AccountID const ammAccount_;
Issue const lptIssue_;
IOUAmount const initialLPTokens_;
public:
AMM(Env& env,
@@ -194,6 +194,12 @@ public:
Issue const& issue2,
std::optional<AccountID> const& account = std::nullopt) const;
std::tuple<STAmount, STAmount, STAmount>
balances(std::optional<AccountID> const& account = std::nullopt) const
{
return balances(asset1_.get<Issue>(), asset2_.get<Issue>(), account);
}
[[nodiscard]] bool
expectLPTokens(AccountID const& account, IOUAmount const& tokens) const;
@@ -428,6 +434,9 @@ private:
[[nodiscard]] bool
expectAuctionSlot(auto&& cb) const;
IOUAmount
initialTokens();
};
namespace amm {

View File

@@ -33,6 +33,15 @@ class AMM;
enum class Fund { All, Acct, Gw, IOUOnly };
struct TestAMMArg
{
std::optional<std::pair<STAmount, STAmount>> pool = std::nullopt;
std::uint16_t tfee = 0;
std::optional<jtx::ter> ter = std::nullopt;
std::vector<FeatureBitset> features = {supported_amendments()};
bool noLog = false;
};
void
fund(
jtx::Env& env,
@@ -85,6 +94,11 @@ protected:
std::uint16_t tfee = 0,
std::optional<jtx::ter> const& ter = std::nullopt,
std::vector<FeatureBitset> const& features = {supported_amendments()});
void
testAMM(
std::function<void(jtx::AMM&, jtx::Env&)>&& cb,
TestAMMArg const& arg);
};
class AMMTest : public jtx::AMMTestBase

View File

@@ -646,6 +646,12 @@ public:
void
disableFeature(uint256 const feature);
bool
enabled(uint256 feature) const
{
return current()->rules().enabled(feature);
}
private:
void
fund(bool setDefaultRipple, STAmount const& amount, Account const& account);

View File

@@ -18,8 +18,9 @@
//==============================================================================
#include <test/jtx/AMM.h>
#include <test/jtx/Env.h>
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/protocol/AMMCore.h>
@@ -38,12 +39,10 @@ number(STAmount const& a)
return a;
}
static IOUAmount
initialTokens(STAmount const& asset1, STAmount const& asset2)
IOUAmount
AMM::initialTokens()
{
auto const product = number(asset1) * number(asset2);
return (IOUAmount)(product.mantissa() >= 0 ? root2(product)
: root2(-product));
return getLPTokensBalance();
}
AMM::AMM(
@@ -64,7 +63,6 @@ AMM::AMM(
, asset1_(asset1)
, asset2_(asset2)
, ammID_(keylet::amm(asset1_.issue(), asset2_.issue()).key)
, initialLPTokens_(initialTokens(asset1, asset2))
, log_(log)
, doClose_(close)
, lastPurchasePrice_(0)
@@ -77,6 +75,7 @@ AMM::AMM(
asset1_.issue().currency,
asset2_.issue().currency,
ammAccount_))
, initialLPTokens_(initialTokens())
{
}

View File

@@ -17,9 +17,9 @@
*/
//==============================================================================
#include <test/jtx/AMMTest.h>
#include <test/jtx/AMM.h>
#include <test/jtx/AMMTest.h>
#include <test/jtx/CaptureLogs.h>
#include <test/jtx/Env.h>
#include <test/jtx/pay.h>
#include <xrpld/rpc/RPCHandler.h>
@@ -104,15 +104,37 @@ AMMTestBase::testAMM(
std::uint16_t tfee,
std::optional<jtx::ter> const& ter,
std::vector<FeatureBitset> const& vfeatures)
{
testAMM(
std::move(cb),
TestAMMArg{
.pool = pool, .tfee = tfee, .ter = ter, .features = vfeatures});
}
void
AMMTestBase::testAMM(
std::function<void(jtx::AMM&, jtx::Env&)>&& cb,
TestAMMArg const& arg)
{
using namespace jtx;
for (auto const& features : vfeatures)
std::string logs;
for (auto const& features : arg.features)
{
Env env{*this, features};
// Env env{
// *this,
// features,
// arg.noLog ? std::make_unique<CaptureLogs>(&logs) : nullptr};
Env env(
*this,
envconfig(),
features,
nullptr,
beast::severities::kDisabled);
auto const [asset1, asset2] =
pool ? *pool : std::make_pair(XRP(10000), USD(10000));
arg.pool ? *arg.pool : std::make_pair(XRP(10000), USD(10000));
auto tofund = [&](STAmount const& a) -> STAmount {
if (a.native())
{
@@ -142,7 +164,7 @@ AMMTestBase::testAMM(
alice,
asset1,
asset2,
CreateArg{.log = false, .tfee = tfee, .err = ter});
CreateArg{.log = false, .tfee = arg.tfee, .err = arg.ter});
if (BEAST_EXPECT(
ammAlice.expectBalances(asset1, asset2, ammAlice.tokens())))
cb(ammAlice, env);

View File

@@ -203,98 +203,107 @@ public:
}
void
testVoteAndBid()
testVoteAndBid(FeatureBitset features)
{
testcase("Vote and Bid");
using namespace jtx;
testAMM([&](AMM& ammAlice, Env& env) {
BEAST_EXPECT(ammAlice.expectAmmRpcInfo(
XRP(10000), USD(10000), IOUAmount{10000000, 0}));
std::unordered_map<std::string, std::uint16_t> votes;
votes.insert({alice.human(), 0});
for (int i = 0; i < 7; ++i)
{
Account a(std::to_string(i));
votes.insert({a.human(), 50 * (i + 1)});
fund(env, gw, {a}, {USD(10000)}, Fund::Acct);
ammAlice.deposit(a, 10000000);
ammAlice.vote(a, 50 * (i + 1));
}
BEAST_EXPECT(ammAlice.expectTradingFee(175));
Account ed("ed");
Account bill("bill");
env.fund(XRP(1000), bob, ed, bill);
env(ammAlice.bid(
{.bidMin = 100, .authAccounts = {carol, bob, ed, bill}}));
BEAST_EXPECT(ammAlice.expectAmmRpcInfo(
XRP(80000),
USD(80000),
IOUAmount{79994400},
std::nullopt,
std::nullopt,
ammAlice.ammAccount()));
for (auto i = 0; i < 2; ++i)
{
std::unordered_set<std::string> authAccounts = {
carol.human(), bob.human(), ed.human(), bill.human()};
auto const ammInfo = i ? ammAlice.ammRpcInfo()
: ammAlice.ammRpcInfo(
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ammAlice.ammAccount());
auto const& amm = ammInfo[jss::amm];
try
testAMM(
[&](AMM& ammAlice, Env& env) {
BEAST_EXPECT(ammAlice.expectAmmRpcInfo(
XRP(10000), USD(10000), IOUAmount{10000000, 0}));
std::unordered_map<std::string, std::uint16_t> votes;
votes.insert({alice.human(), 0});
for (int i = 0; i < 7; ++i)
{
// votes
auto const voteSlots = amm[jss::vote_slots];
auto votesCopy = votes;
for (std::uint8_t i = 0; i < 8; ++i)
Account a(std::to_string(i));
votes.insert({a.human(), 50 * (i + 1)});
fund(env, gw, {a}, {USD(10001)}, Fund::Acct);
ammAlice.deposit(a, 10000000);
ammAlice.vote(a, 50 * (i + 1));
}
BEAST_EXPECT(ammAlice.expectTradingFee(175));
Account ed("ed");
Account bill("bill");
env.fund(XRP(1000), bob, ed, bill);
env(ammAlice.bid(
{.bidMin = 100, .authAccounts = {carol, bob, ed, bill}}));
BEAST_EXPECT(ammAlice.expectAmmRpcInfo(
XRPAmount(80000000005),
STAmount{USD, UINT64_C(80'000'00000000005), -11},
IOUAmount{79994400},
std::nullopt,
std::nullopt,
ammAlice.ammAccount()));
for (auto i = 0; i < 2; ++i)
{
std::unordered_set<std::string> authAccounts = {
carol.human(), bob.human(), ed.human(), bill.human()};
auto const ammInfo = i ? ammAlice.ammRpcInfo()
: ammAlice.ammRpcInfo(
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt,
ammAlice.ammAccount());
auto const& amm = ammInfo[jss::amm];
try
{
if (!BEAST_EXPECT(
votes[voteSlots[i][jss::account].asString()] ==
voteSlots[i][jss::trading_fee].asUInt() &&
voteSlots[i][jss::vote_weight].asUInt() ==
12500))
// votes
auto const voteSlots = amm[jss::vote_slots];
auto votesCopy = votes;
for (std::uint8_t i = 0; i < 8; ++i)
{
if (!BEAST_EXPECT(
votes[voteSlots[i][jss::account]
.asString()] ==
voteSlots[i][jss::trading_fee]
.asUInt() &&
voteSlots[i][jss::vote_weight].asUInt() ==
12500))
return;
votes.erase(voteSlots[i][jss::account].asString());
}
if (!BEAST_EXPECT(votes.empty()))
return;
votes.erase(voteSlots[i][jss::account].asString());
}
if (!BEAST_EXPECT(votes.empty()))
return;
votes = votesCopy;
votes = votesCopy;
// bid
auto const auctionSlot = amm[jss::auction_slot];
for (std::uint8_t i = 0; i < 4; ++i)
{
if (!BEAST_EXPECT(authAccounts.contains(
// bid
auto const auctionSlot = amm[jss::auction_slot];
for (std::uint8_t i = 0; i < 4; ++i)
{
if (!BEAST_EXPECT(authAccounts.contains(
auctionSlot[jss::auth_accounts][i]
[jss::account]
.asString())))
return;
authAccounts.erase(
auctionSlot[jss::auth_accounts][i][jss::account]
.asString())))
.asString());
}
if (!BEAST_EXPECT(authAccounts.empty()))
return;
authAccounts.erase(
auctionSlot[jss::auth_accounts][i][jss::account]
.asString());
BEAST_EXPECT(
auctionSlot[jss::account].asString() ==
alice.human() &&
auctionSlot[jss::discounted_fee].asUInt() == 17 &&
auctionSlot[jss::price][jss::value].asString() ==
"5600" &&
auctionSlot[jss::price][jss::currency].asString() ==
to_string(ammAlice.lptIssue().currency) &&
auctionSlot[jss::price][jss::issuer].asString() ==
to_string(ammAlice.lptIssue().account));
}
catch (std::exception const& e)
{
fail(e.what(), __FILE__, __LINE__);
}
if (!BEAST_EXPECT(authAccounts.empty()))
return;
BEAST_EXPECT(
auctionSlot[jss::account].asString() == alice.human() &&
auctionSlot[jss::discounted_fee].asUInt() == 17 &&
auctionSlot[jss::price][jss::value].asString() ==
"5600" &&
auctionSlot[jss::price][jss::currency].asString() ==
to_string(ammAlice.lptIssue().currency) &&
auctionSlot[jss::price][jss::issuer].asString() ==
to_string(ammAlice.lptIssue().account));
}
catch (std::exception const& e)
{
fail(e.what(), __FILE__, __LINE__);
}
}
});
},
std::nullopt,
0,
std::nullopt,
{features});
}
void
@@ -337,9 +346,11 @@ public:
void
run() override
{
using namespace jtx;
auto const all = supported_amendments();
testErrors();
testSimpleRpc();
testVoteAndBid();
testVoteAndBid(all);
testFreeze();
testInvalidAmmField();
}

View File

@@ -51,6 +51,8 @@ reduceOffer(auto const& amount)
} // namespace detail
enum class IsDeposit : bool { No = false, Yes = true };
/** Calculate LP Tokens given AMM pool reserves.
* @param asset1 AMM one side of the pool reserve
* @param asset2 AMM another side of the pool reserve
@@ -70,7 +72,7 @@ ammLPTokens(
* @return tokens
*/
STAmount
lpTokensIn(
lpTokensOut(
STAmount const& asset1Balance,
STAmount const& asset1Deposit,
STAmount const& lptAMMBalance,
@@ -99,7 +101,7 @@ ammAssetIn(
* @return tokens out amount
*/
STAmount
lpTokensOut(
lpTokensIn(
STAmount const& asset1Balance,
STAmount const& asset1Withdraw,
STAmount const& lptAMMBalance,
@@ -113,7 +115,7 @@ lpTokensOut(
* @return calculated asset amount
*/
STAmount
withdrawByTokens(
ammAssetOut(
STAmount const& assetBalance,
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
@@ -517,13 +519,13 @@ square(Number const& n);
* withdraw to cancel out the precision loss.
* @param lptAMMBalance LPT AMM Balance
* @param lpTokens LP tokens to deposit or withdraw
* @param isDeposit true if deposit, false if withdraw
* @param isDeposit Yes if deposit, No if withdraw
*/
STAmount
adjustLPTokens(
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
bool isDeposit);
IsDeposit isDeposit);
/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if
* the adjusted LP tokens are less than the provided LP tokens.
@@ -533,7 +535,7 @@ adjustLPTokens(
* @param lptAMMBalance LPT AMM Balance
* @param lpTokens LP tokens to deposit or withdraw
* @param tfee trading fee in basis points
* @param isDeposit true if deposit, false if withdraw
* @param isDeposit Yes if deposit, No if withdraw
* @return
*/
std::tuple<STAmount, std::optional<STAmount>, STAmount>
@@ -544,7 +546,7 @@ adjustAmountsByLPTokens(
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
std::uint16_t tfee,
bool isDeposit);
IsDeposit isDeposit);
/** Positive solution for quadratic equation:
* x = (-b + sqrt(b**2 + 4*a*c))/(2*a)
@@ -552,6 +554,134 @@ adjustAmountsByLPTokens(
Number
solveQuadraticEq(Number const& a, Number const& b, Number const& c);
STAmount
multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm);
namespace detail {
inline Number::rounding_mode
getLPTokenRounding(IsDeposit isDeposit)
{
// Minimize on deposit, maximize on withdraw to ensure
// AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance
return isDeposit == IsDeposit::Yes ? Number::downward : Number::upward;
}
inline Number::rounding_mode
getAssetRounding(IsDeposit isDeposit)
{
// Maximize on deposit, minimize on withdraw to ensure
// AMM invariant sqrt(poolAsset1 * poolAsset2) >= LPTokensBalance
return isDeposit == IsDeposit::Yes ? Number::upward : Number::downward;
}
} // namespace detail
/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas
* calculate the amount as a fractional value of the pool balance. The rounding
* takes place on the last step of multiplying the balance by the fraction if
* AMMv1_3 is enabled.
*/
template <typename A>
STAmount
getRoundedAsset(
Rules const& rules,
STAmount const& balance,
A const& frac,
IsDeposit isDeposit)
{
auto const rm = detail::getAssetRounding(isDeposit);
return multiply(balance, frac, rm);
}
/** Round AMM single deposit/withdrawal amount.
* The lambda's are used to delay evaluation until the function
* is executed so that the calculation is not done twice. noRoundCb() is
* called if AMMv1_3 is disabled. Otherwise, the rounding is set and
* the amount is:
* isDeposit is Yes - the balance multiplied by productCb()
* isDeposit is No - the result of productCb(). The rounding is
* the same for all calculations in productCb()
*/
STAmount
getRoundedAsset(
Rules const& rules,
std::function<Number()>&& noRoundCb,
STAmount const& balance,
std::function<Number()>&& productCb,
IsDeposit isDeposit);
/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas
* calculate the lptokens as a fractional value of the AMM total lptokens.
* The rounding takes place on the last step of multiplying the balance by
* the fraction if AMMv1_3 is enabled. The tokens are then
* adjusted to factor in the loss in precision (we only keep 16 significant
* digits) when adding the lptokens to the balance.
*/
STAmount
getRoundedLPTokens(
Rules const& rules,
STAmount const& balance,
Number const& frac,
IsDeposit isDeposit);
/** Round AMM single deposit/withdrawal LPToken amount.
* The lambda's are used to delay evaluation until the function is executed
* so that the calculations are not done twice.
* noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set
* and the lptokens are:
* if isDeposit is Yes - the result of productCb(). The rounding is
* the same for all calculations in productCb()
* if isDeposit is No - the balance multiplied by productCb()
* The lptokens are then adjusted to factor in the loss in precision
* (we only keep 16 significant digits) when adding the lptokens to the balance.
*/
STAmount
getRoundedLPTokens(
Rules const& rules,
std::function<Number()>&& noRoundCb,
STAmount const& lptAMMBalance,
std::function<Number()>&& productCb,
IsDeposit isDeposit);
/* Next two functions adjust asset in/out amount to factor in the adjusted
* lptokens. The lptokens are calculated from the asset in/out. The lptokens are
* then adjusted to factor in the loss in precision. The adjusted lptokens might
* be less than the initially calculated tokens. Therefore, the asset in/out
* must be adjusted. The rounding might result in the adjusted amount being
* greater than the original asset in/out amount. If this happens,
* then the original amount is reduced by the difference in the adjusted amount
* and the original amount. The actual tokens and the actual adjusted amount
* are then recalculated. The minimum of the original and the actual
* adjusted amount is returned.
*/
std::pair<STAmount, STAmount>
adjustAssetInByTokens(
Rules const& rules,
STAmount const& balance,
STAmount const& amount,
STAmount const& lptAMMBalance,
STAmount const& tokens,
std::uint16_t tfee);
std::pair<STAmount, STAmount>
adjustAssetOutByTokens(
Rules const& rules,
STAmount const& balance,
STAmount const& amount,
STAmount const& lptAMMBalance,
STAmount const& tokens,
std::uint16_t tfee);
/** Find a fraction of tokens after the tokens are adjusted. The fraction
* is used to adjust equal deposit/withdraw amount.
*/
Number
adjustFracByTokens(
Rules const& rules,
STAmount const& lptAMMBalance,
STAmount const& tokens,
Number const& frac);
} // namespace ripple
#endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED

View File

@@ -27,6 +27,9 @@ ammLPTokens(
STAmount const& asset2,
Issue const& lptIssue)
{
// AMM invariant: sqrt(asset1 * asset2) >= LPTokensBalance
auto const rounding = Number::downward;
NumberRoundModeGuard g(rounding);
auto const tokens = root2(asset1 * asset2);
return toSTAmount(lptIssue, tokens);
}
@@ -38,7 +41,7 @@ ammLPTokens(
* where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1
*/
STAmount
lpTokensIn(
lpTokensOut(
STAmount const& asset1Balance,
STAmount const& asset1Deposit,
STAmount const& lptAMMBalance,
@@ -48,8 +51,10 @@ lpTokensIn(
auto const f2 = feeMultHalf(tfee) / f1;
Number const r = asset1Deposit / asset1Balance;
auto const c = root2(f2 * f2 + r / f1) - f2;
auto const t = lptAMMBalance * (r - c) / (1 + c);
return toSTAmount(lptAMMBalance.issue(), t);
// minimize tokens out
auto const frac = (r - c) / (1 + c);
return multiply(lptAMMBalance, frac, Number::downward);
}
/* Equation 4 solves equation 3 for b:
@@ -78,8 +83,10 @@ ammAssetIn(
auto const a = 1 / (t2 * t2);
auto const b = 2 * d / t2 - 1 / f1;
auto const c = d * d - f2 * f2;
return toSTAmount(
asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c));
// maximize deposit
auto const frac = solveQuadraticEq(a, b, c);
return multiply(asset1Balance, frac, Number::upward);
}
/* Equation 7:
@@ -87,7 +94,7 @@ ammAssetIn(
* where R = b/B, c = R*fee + 2 - fee
*/
STAmount
lpTokensOut(
lpTokensIn(
STAmount const& asset1Balance,
STAmount const& asset1Withdraw,
STAmount const& lptAMMBalance,
@@ -96,8 +103,10 @@ lpTokensOut(
Number const fr = asset1Withdraw / asset1Balance;
auto const f1 = getFee(tfee);
auto const c = fr * f1 + 2 - f1;
auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2;
return toSTAmount(lptAMMBalance.issue(), t);
// maximize tokens in
auto const frac = (c - root2(c * c - 4 * fr)) / 2;
return multiply(lptAMMBalance, frac, Number::upward);
}
/* Equation 8 solves equation 7 for b:
@@ -111,7 +120,7 @@ lpTokensOut(
* R = (t1**2 + t1*(f - 2)) / (t1*f - 1)
*/
STAmount
withdrawByTokens(
ammAssetOut(
STAmount const& assetBalance,
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
@@ -119,8 +128,10 @@ withdrawByTokens(
{
auto const f = getFee(tfee);
Number const t1 = lpTokens / lptAMMBalance;
auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1);
return toSTAmount(assetBalance.issue(), b);
// minimize withdraw
auto const frac = (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1);
return multiply(assetBalance, frac, Number::downward);
}
Number
@@ -133,12 +144,12 @@ STAmount
adjustLPTokens(
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
bool isDeposit)
IsDeposit isDeposit)
{
// Force rounding downward to ensure adjusted tokens are less or equal
// to requested tokens.
saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward));
if (isDeposit)
if (isDeposit == IsDeposit::Yes)
return (lptAMMBalance + lpTokens) - lptAMMBalance;
return (lpTokens - lptAMMBalance) + lptAMMBalance;
}
@@ -151,47 +162,10 @@ adjustAmountsByLPTokens(
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
std::uint16_t tfee,
bool isDeposit)
IsDeposit isDeposit)
{
auto const lpTokensActual =
adjustLPTokens(lptAMMBalance, lpTokens, isDeposit);
if (lpTokensActual == beast::zero)
{
auto const amount2Opt =
amount2 ? std::make_optional(STAmount{}) : std::nullopt;
return std::make_tuple(STAmount{}, amount2Opt, lpTokensActual);
}
if (lpTokensActual < lpTokens)
{
// Equal trade
if (amount2)
{
Number const fr = lpTokensActual / lpTokens;
auto const amountActual = toSTAmount(amount.issue(), fr * amount);
auto const amount2Actual =
toSTAmount(amount2->issue(), fr * *amount2);
return std::make_tuple(amountActual, amount2Actual, lpTokensActual);
}
// Single trade
auto const amountActual = [&]() {
if (isDeposit)
return ammAssetIn(
amountBalance, lptAMMBalance, lpTokensActual, tfee);
return withdrawByTokens(
amountBalance, lptAMMBalance, lpTokensActual, tfee);
}();
return std::make_tuple(amountActual, std::nullopt, lpTokensActual);
}
XRPL_ASSERT(
lpTokensActual == lpTokens,
"ripple::adjustAmountsByLPTokens : LP tokens match actual");
return {amount, amount2, lpTokensActual};
// AMMv1_3 amendment adjusts tokens and amounts in deposit/withdraw
return std::make_tuple(amount, amount2, lpTokens);
}
Number
@@ -215,4 +189,117 @@ solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c)
return (2 * c) / (-b + root2(d));
}
STAmount
multiply(STAmount const& amount, Number const& frac, Number::rounding_mode rm)
{
NumberRoundModeGuard g(rm);
auto const t = amount * frac;
return toSTAmount(amount.issue(), t, rm);
}
STAmount
getRoundedAsset(
Rules const& rules,
std::function<Number()>&& noRoundCb,
STAmount const& balance,
std::function<Number()>&& productCb,
IsDeposit isDeposit)
{
auto const rm = detail::getAssetRounding(isDeposit);
if (isDeposit == IsDeposit::Yes)
return multiply(balance, productCb(), rm);
NumberRoundModeGuard g(rm);
return toSTAmount(balance.issue(), productCb(), rm);
}
STAmount
getRoundedLPTokens(
Rules const& rules,
STAmount const& balance,
Number const& frac,
IsDeposit isDeposit)
{
auto const rm = detail::getLPTokenRounding(isDeposit);
auto const tokens = multiply(balance, frac, rm);
return adjustLPTokens(balance, tokens, isDeposit);
}
STAmount
getRoundedLPTokens(
Rules const& rules,
std::function<Number()>&& noRoundCb,
STAmount const& lptAMMBalance,
std::function<Number()>&& productCb,
IsDeposit isDeposit)
{
auto const tokens = [&] {
auto const rm = detail::getLPTokenRounding(isDeposit);
if (isDeposit == IsDeposit::Yes)
{
NumberRoundModeGuard g(rm);
return toSTAmount(lptAMMBalance.issue(), productCb(), rm);
}
return multiply(lptAMMBalance, productCb(), rm);
}();
return adjustLPTokens(lptAMMBalance, tokens, isDeposit);
}
std::pair<STAmount, STAmount>
adjustAssetInByTokens(
Rules const& rules,
STAmount const& balance,
STAmount const& amount,
STAmount const& lptAMMBalance,
STAmount const& tokens,
std::uint16_t tfee)
{
auto assetAdj = ammAssetIn(balance, lptAMMBalance, tokens, tfee);
auto tokensAdj = tokens;
// Rounding didn't work the right way.
// Try to adjust the original deposit amount by difference
// in adjust and original amount. Then adjust tokens and deposit amount.
if (assetAdj > amount)
{
auto const adjAmount = amount - (assetAdj - amount);
auto const t = lpTokensOut(balance, adjAmount, lptAMMBalance, tfee);
tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::Yes);
assetAdj = ammAssetIn(balance, lptAMMBalance, tokensAdj, tfee);
}
return {tokensAdj, std::min(amount, assetAdj)};
}
std::pair<STAmount, STAmount>
adjustAssetOutByTokens(
Rules const& rules,
STAmount const& balance,
STAmount const& amount,
STAmount const& lptAMMBalance,
STAmount const& tokens,
std::uint16_t tfee)
{
auto assetAdj = ammAssetOut(balance, lptAMMBalance, tokens, tfee);
auto tokensAdj = tokens;
// Rounding didn't work the right way.
// Try to adjust the original deposit amount by difference
// in adjust and original amount. Then adjust tokens and deposit amount.
if (assetAdj > amount)
{
auto const adjAmount = amount - (assetAdj - amount);
auto const t = lpTokensIn(balance, adjAmount, lptAMMBalance, tfee);
tokensAdj = adjustLPTokens(lptAMMBalance, t, IsDeposit::No);
assetAdj = ammAssetOut(balance, lptAMMBalance, tokensAdj, tfee);
}
return {tokensAdj, std::min(amount, assetAdj)};
}
Number
adjustFracByTokens(
Rules const& rules,
STAmount const& lptAMMBalance,
STAmount const& tokens,
Number const& frac)
{
return tokens / lptAMMBalance;
}
} // namespace ripple

View File

@@ -79,6 +79,21 @@ AMMBid::preflight(PreflightContext const& ctx)
JLOG(ctx.j.debug()) << "AMM Bid: Invalid number of AuthAccounts.";
return temMALFORMED;
}
else
{
AccountID account = ctx.tx[sfAccount];
std::set<AccountID> unique;
for (auto const& obj : authAccounts)
{
auto authAccount = obj[sfAccount];
if (authAccount == account || unique.contains(authAccount))
{
JLOG(ctx.j.debug()) << "AMM Bid: Invalid auth.account.";
return temMALFORMED;
}
unique.insert(authAccount);
}
}
}
return preflight2(ctx);
@@ -233,7 +248,9 @@ applyBid(
auctionSlot.makeFieldAbsent(sfAuthAccounts);
// Burn the remaining bid amount
auto const saBurn = adjustLPTokens(
lptAMMBalance, toSTAmount(lptAMMBalance.issue(), burn), false);
lptAMMBalance,
toSTAmount(lptAMMBalance.issue(), burn),
IsDeposit::No);
if (saBurn >= lptAMMBalance)
{
// This error case should never occur.

View File

@@ -545,7 +545,7 @@ AMMDeposit::deposit(
lptAMMBalance,
lpTokensDeposit,
tfee,
true);
IsDeposit::Yes);
if (lpTokensDepositActual <= beast::zero)
{
@@ -628,6 +628,15 @@ AMMDeposit::deposit(
return {tesSUCCESS, lptAMMBalance + lpTokensDepositActual};
}
static STAmount
adjustLPTokensOut(
Rules const& rules,
STAmount const& lptAMMBalance,
STAmount const& lpTokensDeposit)
{
return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit::Yes);
}
/** Proportional deposit of pools assets in exchange for the specified
* amount of LPTokens.
*/
@@ -645,16 +654,25 @@ AMMDeposit::equalDepositTokens(
{
try
{
auto const tokensAdj =
adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}};
auto const frac =
divide(lpTokensDeposit, lptAMMBalance, lptAMMBalance.issue());
divide(tokensAdj, lptAMMBalance, lptAMMBalance.issue());
// amounts factor in the adjusted tokens
auto const amountDeposit =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes);
auto const amount2Deposit =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes);
return deposit(
view,
ammAccount,
amountBalance,
multiply(amountBalance, frac, amountBalance.issue()),
multiply(amount2Balance, frac, amount2Balance.issue()),
amountDeposit,
amount2Deposit,
lptAMMBalance,
lpTokensDeposit,
tokensAdj,
depositMin,
deposit2Min,
std::nullopt,
@@ -711,37 +729,49 @@ AMMDeposit::equalDepositLimit(
std::uint16_t tfee)
{
auto frac = Number{amount} / amountBalance;
auto tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac);
if (tokens == beast::zero)
return {tecAMM_FAILED, STAmount{}};
auto const amount2Deposit = amount2Balance * frac;
auto tokensAdj =
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes);
if (tokensAdj == beast::zero)
{
return {tecAMM_INVALID_TOKENS, STAmount{}};
}
// factor in the adjusted tokens
frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac);
auto const amount2Deposit =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::Yes);
if (amount2Deposit <= amount2)
return deposit(
view,
ammAccount,
amountBalance,
amount,
toSTAmount(amount2Balance.issue(), amount2Deposit),
amount2Deposit,
lptAMMBalance,
tokens,
tokensAdj,
std::nullopt,
std::nullopt,
lpTokensDepositMin,
tfee);
frac = Number{amount2} / amount2Balance;
tokens = toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac);
if (tokens == beast::zero)
return {tecAMM_FAILED, STAmount{}};
auto const amountDeposit = amountBalance * frac;
tokensAdj =
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::Yes);
if (tokensAdj == beast::zero)
{
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
}
// factor in the adjusted tokens
frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac);
auto const amountDeposit =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::Yes);
if (amountDeposit <= amount)
return deposit(
view,
ammAccount,
amountBalance,
toSTAmount(amountBalance.issue(), amountDeposit),
amountDeposit,
amount2,
lptAMMBalance,
tokens,
tokensAdj,
std::nullopt,
std::nullopt,
lpTokensDepositMin,
@@ -767,17 +797,27 @@ AMMDeposit::singleDeposit(
std::optional<STAmount> const& lpTokensDepositMin,
std::uint16_t tfee)
{
auto const tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee);
auto const tokens = adjustLPTokensOut(
view.rules(),
lptAMMBalance,
lpTokensOut(amountBalance, amount, lptAMMBalance, tfee));
if (tokens == beast::zero)
return {tecAMM_FAILED, STAmount{}};
{
return {tecAMM_INVALID_TOKENS, STAmount{}};
}
// factor in the adjusted tokens
auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens(
view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
return deposit(
view,
ammAccount,
amountBalance,
amount,
amountDepositAdj,
std::nullopt,
lptAMMBalance,
tokens,
tokensAdj,
std::nullopt,
std::nullopt,
lpTokensDepositMin,
@@ -801,8 +841,13 @@ AMMDeposit::singleDepositTokens(
STAmount const& lpTokensDeposit,
std::uint16_t tfee)
{
auto const tokensAdj =
adjustLPTokensOut(view.rules(), lptAMMBalance, lpTokensDeposit);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}};
// the adjusted tokens are factored in
auto const amountDeposit =
ammAssetIn(amountBalance, lptAMMBalance, lpTokensDeposit, tfee);
ammAssetIn(amountBalance, lptAMMBalance, tokensAdj, tfee);
if (amountDeposit > amount)
return {tecAMM_FAILED, STAmount{}};
return deposit(
@@ -812,7 +857,7 @@ AMMDeposit::singleDepositTokens(
amountDeposit,
std::nullopt,
lptAMMBalance,
lpTokensDeposit,
tokensAdj,
std::nullopt,
std::nullopt,
std::nullopt,
@@ -856,20 +901,29 @@ AMMDeposit::singleDepositEPrice(
{
if (amount != beast::zero)
{
auto const tokens =
lpTokensIn(amountBalance, amount, lptAMMBalance, tfee);
auto const tokens = adjustLPTokensOut(
view.rules(),
lptAMMBalance,
lpTokensOut(amountBalance, amount, lptAMMBalance, tfee));
if (tokens <= beast::zero)
return {tecAMM_FAILED, STAmount{}};
auto const ep = Number{amount} / tokens;
{
return {tecAMM_INVALID_TOKENS, STAmount{}};
}
// factor in the adjusted tokens
auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens(
view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
auto const ep = Number{amountDepositAdj} / tokensAdj;
if (ep <= ePrice)
return deposit(
view,
ammAccount,
amountBalance,
amount,
amountDepositAdj,
std::nullopt,
lptAMMBalance,
tokens,
tokensAdj,
std::nullopt,
std::nullopt,
std::nullopt,
@@ -900,21 +954,37 @@ AMMDeposit::singleDepositEPrice(
auto const a1 = c * c;
auto const b1 = c * c * f2 * f2 + 2 * c - d * d;
auto const c1 = 2 * c * f2 * f2 + 1 - 2 * d * f2;
auto const amountDeposit = toSTAmount(
amountBalance.issue(),
f1 * amountBalance * solveQuadraticEq(a1, b1, c1));
auto amtNoRoundCb = [&] {
return f1 * amountBalance * solveQuadraticEq(a1, b1, c1);
};
auto amtProdCb = [&] { return f1 * solveQuadraticEq(a1, b1, c1); };
auto const amountDeposit = getRoundedAsset(
view.rules(), amtNoRoundCb, amountBalance, amtProdCb, IsDeposit::Yes);
if (amountDeposit <= beast::zero)
return {tecAMM_FAILED, STAmount{}};
auto const tokens =
toSTAmount(lptAMMBalance.issue(), amountDeposit / ePrice);
auto tokNoRoundCb = [&] { return amountDeposit / ePrice; };
auto tokProdCb = [&] { return amountDeposit / ePrice; };
auto const tokens = getRoundedLPTokens(
view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::Yes);
// factor in the adjusted tokens
auto const [tokensAdj, amountDepositAdj] = adjustAssetInByTokens(
view.rules(),
amountBalance,
amountDeposit,
lptAMMBalance,
tokens,
tfee);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
return deposit(
view,
ammAccount,
amountBalance,
amountDeposit,
amountDepositAdj,
std::nullopt,
lptAMMBalance,
tokens,
tokensAdj,
std::nullopt,
std::nullopt,
std::nullopt,

View File

@@ -523,7 +523,7 @@ AMMWithdraw::withdraw(
lpTokensAMMBalance,
lpTokensWithdraw,
tfee,
false);
IsDeposit::No);
return std::make_tuple(
amountWithdraw, amount2Withdraw, lpTokensWithdraw);
}();
@@ -682,6 +682,20 @@ AMMWithdraw::withdraw(
amount2WithdrawActual);
}
static STAmount
adjustLPTokensIn(
Rules const& rules,
STAmount const& lptAMMBalance,
STAmount const& lpTokensWithdraw,
WithdrawAll withdrawAll)
{
if (withdrawAll == WithdrawAll::Yes)
return lpTokensWithdraw;
return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit::No);
}
/** Proportional withdrawal of pool assets for the amount of LPTokens.
*/
std::pair<TER, STAmount>
AMMWithdraw::equalWithdrawTokens(
Sandbox& view,
@@ -785,16 +799,22 @@ AMMWithdraw::equalWithdrawTokens(
journal);
}
auto const frac = divide(lpTokensWithdraw, lptAMMBalance, noIssue());
auto const withdrawAmount =
multiply(amountBalance, frac, amountBalance.issue());
auto const withdraw2Amount =
multiply(amount2Balance, frac, amount2Balance.issue());
auto const tokensAdj = adjustLPTokensIn(
view.rules(), lptAMMBalance, lpTokensWithdraw, withdrawAll);
if (tokensAdj == beast::zero)
return {
tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, std::nullopt};
// the adjusted tokens are factored in
auto const frac = divide(tokensAdj, lptAMMBalance, noIssue());
auto const amountWithdraw =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No);
auto const amount2Withdraw =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No);
// LP is making equal withdrawal by tokens but the requested amount
// of LP tokens is likely too small and results in one-sided pool
// withdrawal due to round off. Fail so the user withdraws
// more tokens.
if (withdrawAmount == beast::zero || withdraw2Amount == beast::zero)
if (amountWithdraw == beast::zero || amount2Withdraw == beast::zero)
return {tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{}};
return withdraw(
@@ -803,10 +823,10 @@ AMMWithdraw::equalWithdrawTokens(
ammAccount,
account,
amountBalance,
withdrawAmount,
withdraw2Amount,
amountWithdraw,
amount2Withdraw,
lptAMMBalance,
lpTokensWithdraw,
tokensAdj,
tfee,
freezeHanding,
withdrawAll,
@@ -861,7 +881,16 @@ AMMWithdraw::equalWithdrawLimit(
std::uint16_t tfee)
{
auto frac = Number{amount} / amountBalance;
auto const amount2Withdraw = amount2Balance * frac;
auto amount2Withdraw =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No);
auto tokensAdj =
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}};
// factor in the adjusted tokens
frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac);
amount2Withdraw =
getRoundedAsset(view.rules(), amount2Balance, frac, IsDeposit::No);
if (amount2Withdraw <= amount2)
{
return withdraw(
@@ -870,26 +899,34 @@ AMMWithdraw::equalWithdrawLimit(
ammAccount,
amountBalance,
amount,
toSTAmount(amount2.issue(), amount2Withdraw),
amount2Withdraw,
lptAMMBalance,
toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac),
tokensAdj,
tfee);
}
frac = Number{amount2} / amount2Balance;
auto const amountWithdraw = amountBalance * frac;
XRPL_ASSERT(
amountWithdraw <= amount,
"ripple::AMMWithdraw::equalWithdrawLimit : maximum amountWithdraw");
auto amountWithdraw =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No);
tokensAdj =
getRoundedLPTokens(view.rules(), lptAMMBalance, frac, IsDeposit::No);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
// factor in the adjusted tokens
frac = adjustFracByTokens(view.rules(), lptAMMBalance, tokensAdj, frac);
amountWithdraw =
getRoundedAsset(view.rules(), amountBalance, frac, IsDeposit::No);
if (amountWithdraw > amount)
return {tecAMM_FAILED, STAmount{}}; // LCOV_EXCL_LINE
return withdraw(
view,
ammSle,
ammAccount,
amountBalance,
toSTAmount(amount.issue(), amountWithdraw),
amountWithdraw,
amount2,
lptAMMBalance,
toSTAmount(lptAMMBalance.issue(), lptAMMBalance * frac),
tokensAdj,
tfee);
}
@@ -908,19 +945,29 @@ AMMWithdraw::singleWithdraw(
STAmount const& amount,
std::uint16_t tfee)
{
auto const tokens = lpTokensOut(amountBalance, amount, lptAMMBalance, tfee);
auto const tokens = adjustLPTokensIn(
view.rules(),
lptAMMBalance,
lpTokensIn(amountBalance, amount, lptAMMBalance, tfee),
isWithdrawAll(ctx_.tx));
if (tokens == beast::zero)
return {tecAMM_FAILED, STAmount{}};
{
return {tecAMM_INVALID_TOKENS, STAmount{}};
}
// factor in the adjusted tokens
auto const [tokensAdj, amountWithdrawAdj] = adjustAssetOutByTokens(
view.rules(), amountBalance, amount, lptAMMBalance, tokens, tfee);
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}}; // LCOV_EXCL_LINE
return withdraw(
view,
ammSle,
ammAccount,
amountBalance,
amount,
amountWithdrawAdj,
std::nullopt,
lptAMMBalance,
tokens,
tokensAdj,
tfee);
}
@@ -945,8 +992,13 @@ AMMWithdraw::singleWithdrawTokens(
STAmount const& lpTokensWithdraw,
std::uint16_t tfee)
{
auto const tokensAdj = adjustLPTokensIn(
view.rules(), lptAMMBalance, lpTokensWithdraw, isWithdrawAll(ctx_.tx));
if (tokensAdj == beast::zero)
return {tecAMM_INVALID_TOKENS, STAmount{}};
// the adjusted tokens are factored in
auto const amountWithdraw =
withdrawByTokens(amountBalance, lptAMMBalance, lpTokensWithdraw, tfee);
ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee);
if (amount == beast::zero || amountWithdraw >= amount)
{
return withdraw(
@@ -957,7 +1009,7 @@ AMMWithdraw::singleWithdrawTokens(
amountWithdraw,
std::nullopt,
lptAMMBalance,
lpTokensWithdraw,
tokensAdj,
tfee);
}
@@ -1006,11 +1058,24 @@ AMMWithdraw::singleWithdrawEPrice(
// t = T*(T + A*E*(f - 2))/(T*f - A*E)
Number const ae = amountBalance * ePrice;
auto const f = getFee(tfee);
auto const tokens = lptAMMBalance * (lptAMMBalance + ae * (f - 2)) /
(lptAMMBalance * f - ae);
if (tokens <= 0)
return {tecAMM_FAILED, STAmount{}};
auto const amountWithdraw = toSTAmount(amount.issue(), tokens / ePrice);
auto tokNoRoundCb = [&] {
return lptAMMBalance * (lptAMMBalance + ae * (f - 2)) /
(lptAMMBalance * f - ae);
};
auto tokProdCb = [&] {
return (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae);
};
auto const tokensAdj = getRoundedLPTokens(
view.rules(), tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit::No);
if (tokensAdj <= beast::zero)
{
return {tecAMM_INVALID_TOKENS, STAmount{}};
}
auto amtNoRoundCb = [&] { return tokensAdj / ePrice; };
auto amtProdCb = [&] { return tokensAdj / ePrice; };
// the adjusted tokens are factored in
auto const amountWithdraw = getRoundedAsset(
view.rules(), amtNoRoundCb, amount, amtProdCb, IsDeposit::No);
if (amount == beast::zero || amountWithdraw >= amount)
{
return withdraw(
@@ -1021,7 +1086,7 @@ AMMWithdraw::singleWithdrawEPrice(
amountWithdraw,
std::nullopt,
lptAMMBalance,
toSTAmount(lptAMMBalance.issue(), tokens),
tokensAdj,
tfee);
}

View File

@@ -301,7 +301,7 @@ private:
std::uint16_t tfee);
/** Check from the flags if it's withdraw all */
WithdrawAll
static WithdrawAll
isWithdrawAll(STTx const& tx);
};

View File

@@ -17,6 +17,9 @@
*/
//==============================================================================
#include <xrpld/app/misc/AMMHelpers.h>
#include <xrpld/app/misc/AMMUtils.h>
#include <xrpld/app/misc/CredentialHelpers.h>
#include <xrpld/app/tx/detail/InvariantCheck.h>
#include <xrpld/app/misc/CredentialHelpers.h>
@@ -1674,4 +1677,309 @@ ValidPermissionedDomain::finalize(
(sleStatus_[1] ? check(*sleStatus_[1], j) : true);
}
void
ValidAMM::visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)
{
if (isDelete)
return;
if (after)
{
auto const type = after->getType();
// AMM object changed
if (type == ltAMM)
{
ammAccount_ = after->getAccountID(sfAccount);
lptAMMBalanceAfter_ = after->getFieldAmount(sfLPTokenBalance);
}
// AMM pool changed
else if (
(type == ltRIPPLE_STATE && after->getFlags() & lsfAMMNode) ||
(type == ltACCOUNT_ROOT && after->isFieldPresent(sfAMMID)))
{
ammPoolChanged_ = true;
}
}
if (before)
{
// AMM object changed
if (before->getType() == ltAMM)
{
lptAMMBalanceBefore_ = before->getFieldAmount(sfLPTokenBalance);
}
}
}
static bool
validBalances(
STAmount const& amount,
STAmount const& amount2,
STAmount const& lptAMMBalance,
ValidAMM::ZeroAllowed zeroAllowed)
{
bool const positive = amount > beast::zero && amount2 > beast::zero &&
lptAMMBalance > beast::zero;
if (zeroAllowed == ValidAMM::ZeroAllowed::Yes)
return positive ||
(amount == beast::zero && amount2 == beast::zero &&
lptAMMBalance == beast::zero);
return positive;
}
bool
ValidAMM::finalizeVote(bool enforce, beast::Journal const& j) const
{
if (lptAMMBalanceAfter_ != lptAMMBalanceBefore_ || ammPoolChanged_)
{
// LPTokens and the pool can not change on vote
// LCOV_EXCL_START
JLOG(j.error()) << "AMMVote invariant failed: "
<< lptAMMBalanceBefore_.value_or(STAmount{}) << " "
<< lptAMMBalanceAfter_.value_or(STAmount{}) << " "
<< ammPoolChanged_;
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::finalizeBid(bool enforce, beast::Journal const& j) const
{
if (ammPoolChanged_)
{
// The pool can not change on bid
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: pool changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
// LPTokens are burnt, therefore there should be fewer LPTokens
else if (
lptAMMBalanceBefore_ && lptAMMBalanceAfter_ &&
(*lptAMMBalanceAfter_ > *lptAMMBalanceBefore_ ||
*lptAMMBalanceAfter_ <= beast::zero))
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMBid invariant failed: " << *lptAMMBalanceBefore_
<< " " << *lptAMMBalanceAfter_;
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::finalizeCreate(
STTx const& tx,
ReadView const& view,
bool enforce,
beast::Journal const& j) const
{
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error())
<< "AMMCreate invariant failed: AMM object is not created";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
else
{
auto const [amount, amount2] = ammPoolHolds(
view,
*ammAccount_,
tx[sfAmount].get<Issue>(),
tx[sfAmount2].get<Issue>(),
fhIGNORE_FREEZE,
j);
// Create invariant:
// sqrt(amount * amount2) == LPTokens
// all balances are greater than zero
if (!validBalances(
amount, amount2, *lptAMMBalanceAfter_, ZeroAllowed::No) ||
ammLPTokens(amount, amount2, lptAMMBalanceAfter_->issue()) !=
*lptAMMBalanceAfter_)
{
JLOG(j.error()) << "AMMCreate invariant failed: " << amount << " "
<< amount2 << " " << *lptAMMBalanceAfter_;
if (enforce)
return false;
}
}
return true;
}
bool
ValidAMM::finalizeDelete(bool enforce, TER res, beast::Journal const& j) const
{
if (ammAccount_)
{
// LCOV_EXCL_START
std::string const msg = (res == tesSUCCESS)
? "AMM object is not deleted on tesSUCCESS"
: "AMM object is changed on tecINCOMPLETE";
JLOG(j.error()) << "AMMDelete invariant failed: " << msg;
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::finalizeDEX(bool enforce, beast::Journal const& j) const
{
if (ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMM swap invariant failed: AMM object changed";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
return true;
}
bool
ValidAMM::generalInvariant(
ripple::STTx const& tx,
ripple::ReadView const& view,
ZeroAllowed zeroAllowed,
beast::Journal const& j) const
{
auto const [amount, amount2] = ammPoolHolds(
view,
*ammAccount_,
tx[sfAsset].get<Issue>(),
tx[sfAsset2].get<Issue>(),
fhIGNORE_FREEZE,
j);
// Deposit and Withdrawal invariant:
// sqrt(amount * amount2) >= LPTokens
// all balances are greater than zero
// unless on last withdrawal
auto const poolProductMean = root2(amount * amount2);
bool const nonNegativeBalances =
validBalances(amount, amount2, *lptAMMBalanceAfter_, zeroAllowed);
bool const strongInvariantCheck = poolProductMean >= *lptAMMBalanceAfter_;
// Allow for a small relative error if strongInvariantCheck fails
auto weakInvariantCheck = [&]() {
return *lptAMMBalanceAfter_ != beast::zero &&
withinRelativeDistance(
poolProductMean, Number{*lptAMMBalanceAfter_}, Number{1, -11});
};
if (!nonNegativeBalances ||
(!strongInvariantCheck && !weakInvariantCheck()))
{
JLOG(j.error()) << "AMM " << tx.getTxnType() << " invariant failed: "
<< tx.getHash(HashPrefix::transactionID) << " "
<< ammPoolChanged_ << " " << amount << " " << amount2
<< " " << poolProductMean << " "
<< lptAMMBalanceAfter_->getText() << " "
<< ((*lptAMMBalanceAfter_ == beast::zero)
? Number{1}
: ((*lptAMMBalanceAfter_ - poolProductMean) /
poolProductMean));
return false;
}
return true;
}
bool
ValidAMM::finalizeDeposit(
ripple::STTx const& tx,
ripple::ReadView const& view,
bool enforce,
beast::Journal const& j) const
{
if (!ammAccount_)
{
// LCOV_EXCL_START
JLOG(j.error()) << "AMMDeposit invariant failed: AMM object is deleted";
if (enforce)
return false;
// LCOV_EXCL_STOP
}
else if (!generalInvariant(tx, view, ZeroAllowed::No, j) && enforce)
return false;
return true;
}
bool
ValidAMM::finalizeWithdraw(
ripple::STTx const& tx,
ripple::ReadView const& view,
bool enforce,
beast::Journal const& j) const
{
if (!ammAccount_)
{
// Last Withdraw or Clawback deleted AMM
}
else if (!generalInvariant(tx, view, ZeroAllowed::Yes, j))
{
if (enforce)
return false;
}
return true;
}
bool
ValidAMM::finalize(
STTx const& tx,
TER const result,
XRPAmount const,
ReadView const& view,
beast::Journal const& j)
{
// Delete may return tecINCOMPLETE if there are too many
// trustlines to delete.
if (result != tesSUCCESS && result != tecINCOMPLETE)
return true;
bool const enforce = true; // view.rules().enabled(fixAMMv1_3);
switch (tx.getTxnType())
{
case ttAMM_CREATE:
return finalizeCreate(tx, view, enforce, j);
case ttAMM_DEPOSIT:
return finalizeDeposit(tx, view, enforce, j);
case ttAMM_CLAWBACK:
case ttAMM_WITHDRAW:
return finalizeWithdraw(tx, view, enforce, j);
case ttAMM_BID:
return finalizeBid(enforce, j);
case ttAMM_VOTE:
return finalizeVote(enforce, j);
case ttAMM_DELETE:
return finalizeDelete(enforce, result, j);
case ttCHECK_CASH:
case ttOFFER_CREATE:
case ttPAYMENT:
return finalizeDEX(enforce, j);
default:
break;
}
return true;
}
} // namespace ripple

View File

@@ -617,6 +617,69 @@ public:
beast::Journal const&);
};
class ValidAMM
{
std::optional<AccountID> ammAccount_;
std::optional<STAmount> lptAMMBalanceAfter_;
std::optional<STAmount> lptAMMBalanceBefore_;
bool ammPoolChanged_;
public:
enum class ZeroAllowed : bool { No = false, Yes = true };
ValidAMM() : ammPoolChanged_{false}
{
}
void
visitEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&);
bool
finalize(
STTx const&,
TER const,
XRPAmount const,
ReadView const&,
beast::Journal const&);
private:
bool
finalizeBid(bool enforce, beast::Journal const&) const;
bool
finalizeVote(bool enforce, beast::Journal const&) const;
bool
finalizeCreate(
STTx const&,
ReadView const&,
bool enforce,
beast::Journal const&) const;
bool
finalizeDelete(bool enforce, TER res, beast::Journal const&) const;
bool
finalizeDeposit(
STTx const&,
ReadView const&,
bool enforce,
beast::Journal const&) const;
// Includes clawback
bool
finalizeWithdraw(
STTx const&,
ReadView const&,
bool enforce,
beast::Journal const&) const;
bool
finalizeDEX(bool enforce, beast::Journal const&) const;
bool
generalInvariant(
STTx const&,
ReadView const&,
ZeroAllowed zeroAllowed,
beast::Journal const&) const;
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
@@ -636,7 +699,8 @@ using InvariantChecks = std::tuple<
NFTokenCountTracking,
ValidClawback,
ValidMPTIssuance,
ValidPermissionedDomain>;
ValidPermissionedDomain,
ValidAMM>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -21,6 +21,8 @@
#define RIPPLE_APP_BOOK_OFFER_H_INCLUDED
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/contract.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/Rules.h>
@@ -169,8 +171,21 @@ public:
* always returns true.
*/
bool
checkInvariant(TAmounts<TIn, TOut> const&, beast::Journal j) const
checkInvariant(TAmounts<TIn, TOut> const& consumed, beast::Journal j) const
{
if (consumed.in > m_amounts.in || consumed.out > m_amounts.out)
{
// LCOV_EXCL_START
JLOG(j.error())
<< "AMMOffer::checkInvariant failed: consumed "
<< to_string(consumed.in) << " " << to_string(consumed.out)
<< " amounts " << to_string(m_amounts.in) << " "
<< to_string(m_amounts.out);
return false;
// LCOV_EXCL_STOP
}
return true;
}
};