additional cleanup

This commit is contained in:
Vito
2026-02-26 17:08:40 +01:00
parent 1c7daa183c
commit 9eecce668d

View File

@@ -48,7 +48,7 @@ public:
DepositResult
deposit(STAmount const& assets)
{
Number const depositAmount = static_cast<Number>(assets);
Number const depositAmount = assets;
Number shares;
Number actualAssets;
@@ -82,13 +82,11 @@ public:
STAmount
redeem(STAmount const& shares)
{
Number const shareAmount = static_cast<Number>(shares);
Number const shareAmount = shares;
// assets = shares * withdrawalNAV / sharesTotal
Number const withdrawalNAV =
assetsTotal_ - interestUnrealized_ - lossUnrealized_;
Number const assetsOut =
(shareAmount * withdrawalNAV) / sharesTotal_;
Number const withdrawalNAV = assetsTotal_ - interestUnrealized_ - lossUnrealized_;
Number const assetsOut = (shareAmount * withdrawalNAV) / sharesTotal_;
assetsTotal_ -= assetsOut;
assetsAvailable_ -= assetsOut;
@@ -100,15 +98,13 @@ public:
WithdrawResult
withdraw(STAmount const& assetsRequested)
{
Number const requestedAmount = static_cast<Number>(assetsRequested);
Number const requestedAmount = assetsRequested;
Number const withdrawalNAV =
assetsTotal_ - interestUnrealized_ - lossUnrealized_;
Number const withdrawalNAV = assetsTotal_ - interestUnrealized_ - lossUnrealized_;
// shares = round_nearest(requested * sharesTotal / withdrawalNAV)
Number const rawShares =
(requestedAmount * sharesTotal_) / withdrawalNAV;
// Round to nearest integer via int64_t cast
Number const rawShares = (requestedAmount * sharesTotal_) / withdrawalNAV;
// Number::operator int64_t() rounds to nearest (not standard C++ truncation)
Number const shares{static_cast<std::int64_t>(rawShares)};
// Recalculate actual assets out
@@ -136,6 +132,13 @@ public:
lossUnrealized_ = iota;
}
void
addRealizedInterest(Number amount)
{
assetsTotal_ += amount;
assetsAvailable_ += amount;
}
STAmount
assetsTotal() const
{
@@ -175,13 +178,6 @@ public:
class VaultSharePricing_test : public beast::unit_test::suite
{
// Helper to get the numeric Number value from an STAmount
static Number
num(STAmount const& a)
{
return static_cast<Number>(a);
}
public:
void
testInitialDepositIOU()
@@ -194,10 +190,10 @@ public:
auto const [shares, assets] = vault.deposit(STAmount{usd, 100});
// 100 * 10^6 = 100,000,000 shares
BEAST_EXPECT(num(shares) == Number(100'000'000));
BEAST_EXPECT(num(assets) == Number(100));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(100));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(100'000'000));
BEAST_EXPECT(Number(shares) == Number(100'000'000));
BEAST_EXPECT(Number(assets) == Number(100));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(100));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(100'000'000));
}
void
@@ -208,12 +204,11 @@ public:
Vault vault{xrpIssue()};
// 10 XRP = 10,000,000 drops; scale=0 so shares = drops 1:1
auto const [shares, assets] =
vault.deposit(STAmount{xrpIssue(), 10'000'000});
auto const [shares, assets] = vault.deposit(STAmount{xrpIssue(), 10'000'000});
BEAST_EXPECT(num(shares) == Number(10'000'000));
BEAST_EXPECT(num(assets) == Number(10'000'000));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(10'000'000));
BEAST_EXPECT(Number(shares) == Number(10'000'000));
BEAST_EXPECT(Number(assets) == Number(10'000'000));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(10'000'000));
}
void
@@ -227,9 +222,9 @@ public:
auto const [shares, assets] = vault.deposit(STAmount{mptAsset, 500});
// scale=0, so 500 assets → 500 shares
BEAST_EXPECT(num(shares) == Number(500));
BEAST_EXPECT(num(assets) == Number(500));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(500));
BEAST_EXPECT(Number(shares) == Number(500));
BEAST_EXPECT(Number(assets) == Number(500));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(500));
}
void
@@ -246,9 +241,9 @@ public:
// Second deposit: 50 assets at same price → 50M shares
auto const [shares, assets] = vault.deposit(STAmount{usd, 50});
BEAST_EXPECT(num(shares) == Number(50'000'000));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(150));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(150'000'000));
BEAST_EXPECT(Number(shares) == Number(50'000'000));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(150));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(150'000'000));
}
void
@@ -262,12 +257,11 @@ public:
vault.deposit(STAmount{usd, 100});
// Redeem half the shares
auto const assetsOut =
vault.redeem(STAmount{vault.shareAsset(), 50'000'000});
auto const assetsOut = vault.redeem(STAmount{vault.shareAsset(), 50'000'000});
BEAST_EXPECT(num(assetsOut) == Number(50));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(50));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(50'000'000));
BEAST_EXPECT(Number(assetsOut) == Number(50));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(50));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(50'000'000));
}
void
@@ -281,12 +275,12 @@ public:
vault.deposit(STAmount{usd, 100});
// Withdraw 50 assets
auto const [shares, assets] =
vault.withdraw(STAmount{usd, 50});
auto const [shares, assets] = vault.withdraw(STAmount{usd, 50});
BEAST_EXPECT(num(shares) == Number(50'000'000));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(50));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(50'000'000));
BEAST_EXPECT(Number(shares) == Number(50'000'000));
BEAST_EXPECT(Number(assets) == Number(50));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(50));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(50'000'000));
}
void
@@ -299,7 +293,7 @@ public:
// Seed vault: 1000 assets, 1000 shares
vault.deposit(STAmount{usd, 1000});
BEAST_EXPECT(num(vault.sharesTotal()) == Number(1000));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1000));
// Protocol adds unrealized interest (omega = 50)
vault.setInterestUnrealized(Number(50));
@@ -308,9 +302,9 @@ public:
// Deposit 95 assets: shares = floor(95 * 1000 / 950) = floor(100) = 100
auto const [shares, assets] = vault.deposit(STAmount{usd, 95});
BEAST_EXPECT(num(shares) == Number(100));
BEAST_EXPECT(Number(shares) == Number(100));
// Recalculated assets: 100 * 950 / 1000 = 95
BEAST_EXPECT(num(assets) == Number(95));
BEAST_EXPECT(Number(assets) == Number(95));
}
void
@@ -327,10 +321,9 @@ public:
// Withdrawal NAV = 1000 - 50 - 100 = 850
// Redeem 100 shares: assets = 100 * 850 / 1000 = 85
auto const assetsOut =
vault.redeem(STAmount{vault.shareAsset(), 100});
auto const assetsOut = vault.redeem(STAmount{vault.shareAsset(), 100});
BEAST_EXPECT(num(assetsOut) == Number(85));
BEAST_EXPECT(Number(assetsOut) == Number(85));
}
void
@@ -351,15 +344,19 @@ public:
// So 1 share costs 0.95 assets to deposit
// Depositing 0.95 assets: shares = floor(0.95 * 1000 / 950) = floor(1) = 1
{
auto const [shares, assets] =
vault.deposit(STAmount{usd, 95, -2}); // 0.95
BEAST_EXPECT(num(shares) == Number(1));
auto const [shares, assets] = vault.deposit(STAmount{usd, 95, -2}); // 0.95
BEAST_EXPECT(Number(shares) == Number(1));
}
// Withdrawal share price = (assetsTotal - omega - iota) / sharesTotal
// ≈ 850 / 1001 ≈ 0.849...
// Redeem 1 share: assets ≈ 0.849...
// (accounting for the extra share and assets from the deposit above)
// After deposit: assetsTotal = 1000.95, sharesTotal = 1001
// withdrawalNAV = assetsTotal - omega - iota = 1000.95 - 50 - 100 = 850.95
// Redeem 1 share: assetsOut = 1 * 850.95 / 1001
{
Number const expected = Number(85095, -2) / Number(1001);
STAmount const expectedAmt(usd, expected);
auto const redeemAssets = vault.redeem(STAmount{vault.shareAsset(), 1});
BEAST_EXPECT(redeemAssets == expectedAmt);
}
}
void
@@ -368,14 +365,6 @@ public:
testcase("Deposit rounding - shares floor");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd, 0};
// Seed: 3 assets, 3 shares
vault.deposit(STAmount{usd, 3});
// Deposit 1 asset: shares = floor(1 * 3 / 3) = 1
// Now try a case that produces fractional shares:
// Seed a vault with 10 assets, 7 shares (custom ratio)
Vault vault2{usd, 0};
vault2.deposit(STAmount{usd, 10});
// 10 assets, 10 shares
@@ -385,9 +374,9 @@ public:
// depositNAV = 10 - 3 = 7
// Deposit 1: shares = floor(1 * 10 / 7) = floor(1.4285...) = 1
auto const [shares, assets] = vault2.deposit(STAmount{usd, 1});
BEAST_EXPECT(num(shares) == Number(1));
BEAST_EXPECT(Number(shares) == Number(1));
// Recalculated assets: 1 * 7 / 10 = 0.7
BEAST_EXPECT(num(assets) == Number(7, -1));
BEAST_EXPECT(Number(assets) == Number(7, -1));
}
void
@@ -403,7 +392,16 @@ public:
// withdrawalNAV = 10 - 3 = 7
// Withdraw 1 asset: shares = round(1 * 10 / 7) = round(1.4285...) = 1
auto const [shares, assets] = vault.withdraw(STAmount{usd, 1});
BEAST_EXPECT(num(shares) == Number(1));
BEAST_EXPECT(Number(shares) == Number(1));
// Round up case: rawShares = 1.1*10/7 ≈ 1.571 → nearest = 2
Vault vault2{usd, 0};
vault2.deposit(STAmount{usd, 10});
vault2.setInterestUnrealized(Number(3));
auto const [shares2, assets2] = vault2.withdraw(STAmount{usd, 11, -1}); // 1.1
BEAST_EXPECT(Number(shares2) == Number(2));
// assetsOut = 2*7/10 = 1.4
BEAST_EXPECT(Number(assets2) == Number(14, -1));
}
void
@@ -415,12 +413,12 @@ public:
Vault vault{usd};
vault.deposit(STAmount{usd, 100});
auto const assetsOut =
vault.redeem(STAmount{vault.shareAsset(), 100'000'000});
auto const assetsOut = vault.redeem(STAmount{vault.shareAsset(), 100'000'000});
BEAST_EXPECT(num(assetsOut) == Number(100));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(0));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(0));
BEAST_EXPECT(Number(assetsOut) == Number(100));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(0));
BEAST_EXPECT(Number(vault.assetsAvailable()) == Number(0));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(0));
}
void
@@ -436,12 +434,11 @@ public:
// Deposit NAV = 1000 - 0 = 1000 (loss doesn't affect deposit)
auto const [depShares, depAssets] = vault.deposit(STAmount{usd, 100});
BEAST_EXPECT(num(depShares) == Number(100));
BEAST_EXPECT(Number(depShares) == Number(100));
// Withdrawal NAV = 1100 - 0 - 200 = 900 (total is now 1100)
// Redeem 100 shares: assets = 100 * 900 / 1100 ≈ 81.818...
auto const redeemAssets =
vault.redeem(STAmount{vault.shareAsset(), 100});
auto const redeemAssets = vault.redeem(STAmount{vault.shareAsset(), 100});
// 100 * 900 / 1100 = 81.8181...
// Compare as STAmount to account for IOU normalization
Number const expected = (Number(100) * Number(900)) / Number(1100);
@@ -455,35 +452,53 @@ public:
testcase("Multiple depositors with yield (IOU)");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd};
Vault vault{usd, 0}; // scale=0 for clean integer math
// User A deposits 100
// User A deposits 100 → 100 shares
auto const [sharesA, assetsA] = vault.deposit(STAmount{usd, 100});
BEAST_EXPECT(num(sharesA) == Number(100'000'000));
BEAST_EXPECT(Number(sharesA) == Number(100));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(100));
// Protocol earns 10 in realized interest (assetsTotal goes up)
// Simulate by: a protocol repays 10, increasing assetsTotal
// We don't have a direct setter for assetsTotal, but we can
// approximate this scenario through the pricing mechanism.
// Instead, let's verify through deposit pricing.
// Protocol realizes 10 interest: assetsTotal becomes 110
vault.addRealizedInterest(Number(10));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(110));
// User B deposits 100 at the same rate (no yield yet)
// User B deposits 100: depositNAV=110, shares=floor(100*100/110)=90
// actualAssets = 90*110/100 = 99
auto const [sharesB, assetsB] = vault.deposit(STAmount{usd, 100});
BEAST_EXPECT(num(sharesB) == Number(100'000'000));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(200));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(200'000'000));
BEAST_EXPECT(Number(sharesB) == Number(90));
BEAST_EXPECT(Number(assetsB) == Number(99));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(209));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(190));
// Both redeem: should get back 100 each
auto const outA =
vault.redeem(STAmount{vault.shareAsset(), 100'000'000});
BEAST_EXPECT(num(outA) == Number(100));
// A redeems 100 shares: assetsOut = 100 * 209 / 190 = 110
auto const outA = vault.redeem(STAmount{vault.shareAsset(), 100});
BEAST_EXPECT(Number(outA) == Number(110));
auto const outB =
vault.redeem(STAmount{vault.shareAsset(), 100'000'000});
BEAST_EXPECT(num(outB) == Number(100));
// B redeems 90 shares: assetsOut = 90 * 99 / 90 = 99
auto const outB = vault.redeem(STAmount{vault.shareAsset(), 90});
BEAST_EXPECT(Number(outB) == Number(99));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(0));
BEAST_EXPECT(num(vault.sharesTotal()) == Number(0));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(0));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(0));
}
void
testPrecisionLoss()
{
testcase("Precision loss - zero shares condition");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd, 0};
// Initial deposit: 1 asset → 1 share
vault.deposit(STAmount{usd, 1});
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1));
// Deposit 0.0001 assets: shares = floor(0.0001 * 1 / 1) = 0
// This is the zero-shares condition (tecPRECISION_LOSS in real implementation)
auto const [shares, assets] = vault.deposit(STAmount{usd, 1, -4});
BEAST_EXPECT(Number(shares) == Number(0));
}
void
@@ -494,21 +509,19 @@ public:
Vault vault{xrpIssue()};
// Deposit 10 XRP = 10M drops
auto const [shares1, assets1] =
vault.deposit(STAmount{xrpIssue(), 10'000'000});
BEAST_EXPECT(num(shares1) == Number(10'000'000));
auto const [shares1, assets1] = vault.deposit(STAmount{xrpIssue(), 10'000'000});
BEAST_EXPECT(Number(shares1) == Number(10'000'000));
// Withdraw 5M drops
auto const [wShares, wAssets] =
vault.withdraw(STAmount{xrpIssue(), 5'000'000});
BEAST_EXPECT(num(wShares) == Number(5'000'000));
BEAST_EXPECT(num(wAssets) == Number(5'000'000));
auto const [wShares, wAssets] = vault.withdraw(STAmount{xrpIssue(), 5'000'000});
BEAST_EXPECT(Number(wShares) == Number(5'000'000));
BEAST_EXPECT(Number(wAssets) == Number(5'000'000));
// Redeem remaining 5M shares
auto const rAssets =
vault.redeem(STAmount{vault.shareAsset(), 5'000'000});
BEAST_EXPECT(num(rAssets) == Number(5'000'000));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(0));
auto const rAssets = vault.redeem(STAmount{vault.shareAsset(), 5'000'000});
BEAST_EXPECT(Number(rAssets) == Number(5'000'000));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(0));
BEAST_EXPECT(Number(vault.assetsAvailable()) == Number(0));
}
void
@@ -520,16 +533,261 @@ public:
Vault vault{mptAsset};
auto const [shares1, assets1] = vault.deposit(STAmount{mptAsset, 1000});
BEAST_EXPECT(num(shares1) == Number(1000));
BEAST_EXPECT(Number(shares1) == Number(1000));
auto const [wShares, wAssets] =
vault.withdraw(STAmount{mptAsset, 400});
BEAST_EXPECT(num(wShares) == Number(400));
auto const [wShares, wAssets] = vault.withdraw(STAmount{mptAsset, 400});
BEAST_EXPECT(Number(wShares) == Number(400));
auto const rAssets =
vault.redeem(STAmount{vault.shareAsset(), 600});
BEAST_EXPECT(num(rAssets) == Number(600));
BEAST_EXPECT(num(vault.assetsTotal()) == Number(0));
auto const rAssets = vault.redeem(STAmount{vault.shareAsset(), 600});
BEAST_EXPECT(Number(rAssets) == Number(600));
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(0));
}
void
testTinyDepositIntoLargeVault()
{
testcase("Tiny deposit into large vault (IOU)");
// Large vault with 1 billion assets at scale=6
// sharesTotal = 1e9 * 1e6 = 1e15
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd}; // scale=6
vault.deposit(STAmount{usd, UINT64_C(1'000'000'000)});
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1, 15));
// Tiny deposit: 0.000001 (1e-6) into a 1e9 vault
// shares = floor(1e-6 * 1e15 / 1e9) = floor(1e0) = 1
auto const [shares, assets] =
vault.deposit(STAmount{usd, 1, -6});
BEAST_EXPECT(Number(shares) == Number(1));
// Recalculated assets: 1 * 1e9 / 1e15 = 1e-6
BEAST_EXPECT(Number(assets) == Number(1, -6));
// Redeem the 1 share back
auto const out = vault.redeem(STAmount{vault.shareAsset(), 1});
BEAST_EXPECT(Number(out) == Number(1, -6));
}
void
testTinyDepositIntoLargeVaultXRP()
{
testcase("Tiny deposit into large vault (XRP)");
// 100 million XRP = 1e14 drops, scale=0 so shares = 1e14
Vault vault{xrpIssue()};
vault.deposit(STAmount{xrpIssue(), UINT64_C(100'000'000'000'000)});
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1, 14));
// Deposit 1 drop into a 1e14-drop vault
// shares = floor(1 * 1e14 / 1e14) = 1
auto const [shares, assets] =
vault.deposit(STAmount{xrpIssue(), 1});
BEAST_EXPECT(Number(shares) == Number(1));
BEAST_EXPECT(Number(assets) == Number(1));
// Redeem the 1 share
auto const out = vault.redeem(STAmount{vault.shareAsset(), 1});
BEAST_EXPECT(Number(out) == Number(1));
}
void
testLargeDepositIntoTinyVault()
{
testcase("Large deposit into tiny vault (IOU)");
// Tiny vault: 0.001 assets, scale=6 → shares = 0.001 * 1e6 = 1000
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd};
vault.deposit(STAmount{usd, 1, -3});
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1000));
// Massive deposit: 1 billion assets
// shares = floor(1e9 * 1000 / 0.001) = floor(1e15) = 1e15
auto const [shares, assets] =
vault.deposit(STAmount{usd, UINT64_C(1'000'000'000)});
BEAST_EXPECT(Number(shares) == Number(1, 15));
BEAST_EXPECT(Number(assets) == Number(1, 9));
// Vault now: assetsTotal ≈ 1e9, sharesTotal = 1e15 + 1000
// Redeem original 1000 shares
// assetsOut = 1000 * assetsTotal / sharesTotal
// The original depositor should get back roughly 0.001
auto const out = vault.redeem(STAmount{vault.shareAsset(), 1000});
// 1000 * (1e9 + 0.001) / (1e15 + 1000) ≈ 0.001
// Precision loss means the exact value depends on Number rounding
Number const expected =
(Number(1000) * Number(vault.assetsTotal())) /
(Number(vault.sharesTotal()) + Number(out));
STAmount const expectedAmt(usd, expected);
BEAST_EXPECT(out == expectedAmt);
}
void
testLargeDepositIntoTinyVaultXRP()
{
testcase("Large deposit into tiny vault (XRP)");
// Tiny vault: 1 drop, scale=0 → 1 share
Vault vault{xrpIssue()};
vault.deposit(STAmount{xrpIssue(), 1});
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1));
// Massive deposit: 1e14 drops (100M XRP)
// shares = floor(1e14 * 1 / 1) = 1e14
auto const [shares, assets] =
vault.deposit(STAmount{xrpIssue(), UINT64_C(100'000'000'000'000)});
BEAST_EXPECT(Number(shares) == Number(1, 14));
BEAST_EXPECT(Number(assets) == Number(1, 14));
// Redeem original 1 share: assetsOut = 1 * (1e14+1) / (1e14+1) = 1
auto const out = vault.redeem(STAmount{vault.shareAsset(), 1});
BEAST_EXPECT(Number(out) == Number(1));
}
void
testHighPrecisionWithInterest()
{
testcase("High precision deposit/redeem with unrealized interest");
// Large vault with interest: tests that tiny depositors aren't
// short-changed or over-paid when NAV diverges from 1:1
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd, 0}; // scale=0
// Seed: 1e12 assets → 1e12 shares
vault.deposit(STAmount{usd, UINT64_C(1'000'000'000'000)});
// Large unrealized interest: 50% of total
vault.setInterestUnrealized(Number(5, 11));
// depositNAV = 1e12 - 5e11 = 5e11
// Tiny deposit: 1 asset
// shares = floor(1 * 1e12 / 5e11) = floor(2) = 2
auto const [shares, assets] = vault.deposit(STAmount{usd, 1});
BEAST_EXPECT(Number(shares) == Number(2));
// Recalculated assets: 2 * 5e11 / 1e12 = 1
BEAST_EXPECT(Number(assets) == Number(1));
// Redeem those 2 shares
// withdrawalNAV = (1e12 + 1) - 5e11 - 0 = 5e11 + 1
// assetsOut = 2 * (5e11 + 1) / (1e12 + 2)
auto const out = vault.redeem(STAmount{vault.shareAsset(), 2});
// Should get back ≈ 1 (within rounding)
Number const expectedNum =
(Number(2) * (Number(1, 12) + Number(1) - Number(5, 11))) /
(Number(1, 12) + Number(2));
STAmount const expectedAmt(usd, expectedNum);
BEAST_EXPECT(out == expectedAmt);
}
void
testHighPrecisionWithLoss()
{
testcase("High precision redeem with unrealized loss");
// Large vault with loss: tests that a tiny share redemption
// correctly accounts for the discounted withdrawalNAV
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd, 0};
vault.deposit(STAmount{usd, UINT64_C(1'000'000'000'000)});
// 20% unrealized loss
vault.setLossUnrealized(Number(2, 11));
// withdrawalNAV = 1e12 - 0 - 2e11 = 8e11
// Redeem 1 share out of 1e12
// assetsOut = 1 * 8e11 / 1e12 = 0.8
auto const out = vault.redeem(STAmount{vault.shareAsset(), 1});
Number const expectedNum = Number(8, 11) / Number(1, 12);
STAmount const expectedAmt(usd, expectedNum);
BEAST_EXPECT(out == expectedAmt);
}
void
testManyTinyDepositsLargeVault()
{
testcase("Many tiny deposits into large vault then full redeem");
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd, 0}; // scale=0
// Seed with 1e9 assets
vault.deposit(STAmount{usd, UINT64_C(1'000'000'000)});
Number totalTinyShares{0};
Number totalTinyAssets{0};
// 100 tiny deposits of 1 asset each
for (int i = 0; i < 100; ++i)
{
auto const [s, a] = vault.deposit(STAmount{usd, 1});
totalTinyShares += Number(s);
totalTinyAssets += Number(a);
}
// Each deposit: shares = floor(1 * sharesTotal / assetsTotal)
// First tiny: floor(1 * 1e9 / 1e9) = 1 share per deposit
BEAST_EXPECT(totalTinyShares == Number(100));
BEAST_EXPECT(totalTinyAssets == Number(100));
// Redeem all 100 tiny shares
auto const out = vault.redeem(STAmount{vault.shareAsset(), 100});
// assetsOut = 100 * (1e9 + 100) / (1e9 + 100) = 100
BEAST_EXPECT(Number(out) == Number(100));
}
void
testExtremeLiquidityRatioIOU()
{
testcase("Extreme liquidity ratio - 1e15 to 1 (IOU)");
// Vault with maximum practical IOU assets at scale=0
Issue const usd{Currency(0x5553440000000000), AccountID(0x4985601)};
Vault vault{usd, 0};
// 1e15 assets → 1e15 shares
vault.deposit(STAmount{usd, UINT64_C(1'000'000'000'000'000)});
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1, 15));
// Deposit 1 asset: shares = floor(1 * 1e15 / 1e15) = 1
auto const [shares, assets] = vault.deposit(STAmount{usd, 1});
BEAST_EXPECT(Number(shares) == Number(1));
BEAST_EXPECT(Number(assets) == Number(1));
// Redeem that 1 share
auto const out = vault.redeem(STAmount{vault.shareAsset(), 1});
BEAST_EXPECT(Number(out) == Number(1));
// Vault should be back to exactly 1e15
BEAST_EXPECT(Number(vault.assetsTotal()) == Number(1, 15));
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1, 15));
}
void
testExtremeLiquidityRatioMPT()
{
testcase("Extreme liquidity ratio - 1e15 to 1 (MPT)");
MPTIssue const mptAsset{makeMptID(300, AccountID(0x5678))};
Vault vault{mptAsset};
vault.deposit(STAmount{mptAsset, UINT64_C(1'000'000'000'000'000)});
BEAST_EXPECT(Number(vault.sharesTotal()) == Number(1, 15));
auto const [shares, assets] = vault.deposit(STAmount{mptAsset, 1});
BEAST_EXPECT(Number(shares) == Number(1));
auto const out = vault.redeem(STAmount{vault.shareAsset(), 1});
BEAST_EXPECT(Number(out) == Number(1));
}
void
@@ -549,8 +807,18 @@ public:
testRedeemAll();
testLossOnlyVault();
testMultipleDepositorsIOU();
testPrecisionLoss();
testXRPFullCycle();
testMPTFullCycle();
testTinyDepositIntoLargeVault();
testTinyDepositIntoLargeVaultXRP();
testLargeDepositIntoTinyVault();
testLargeDepositIntoTinyVaultXRP();
testHighPrecisionWithInterest();
testHighPrecisionWithLoss();
testManyTinyDepositsLargeVault();
testExtremeLiquidityRatioIOU();
testExtremeLiquidityRatioMPT();
}
};