#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ripple { class Vault_test : public beast::unit_test::suite { using PrettyAsset = ripple::test::jtx::PrettyAsset; using PrettyAmount = ripple::test::jtx::PrettyAmount; static auto constexpr negativeAmount = [](PrettyAsset const& asset) -> PrettyAmount { return {STAmount{asset.raw(), 1ul, 0, true, STAmount::unchecked{}}, ""}; }; void testSequences() { using namespace test::jtx; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; Account charlie{"charlie"}; // authorized 3rd party Account dave{"dave"}; auto const testSequence = [&, this]( std::string const& prefix, Env& env, Vault& vault, PrettyAsset const& asset) { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfData] = "AFEED00E"; tx[sfAssetsMaximum] = asset(100).number(); env(tx); env.close(); BEAST_EXPECT(env.le(keylet)); std::uint64_t const scale = asset.raw().holds() ? 1 : 1e6; auto const [share, vaultAccount] = [&env, keylet = keylet, asset, this]() -> std::tuple { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); if (asset.raw().holds() && !asset.raw().native()) BEAST_EXPECT(vault->at(sfScale) == 6); else BEAST_EXPECT(vault->at(sfScale) == 0); auto const shares = env.le(keylet::mptIssuance(vault->at(sfShareMPTID))); BEAST_EXPECT(shares != nullptr); if (asset.raw().holds() && !asset.raw().native()) BEAST_EXPECT(shares->at(sfAssetScale) == 6); else BEAST_EXPECT(shares->at(sfAssetScale) == 0); return { MPTIssue(vault->at(sfShareMPTID)), Account("vault", vault->at(sfAccount))}; }(); auto const shares = share.raw().get(); env.memoize(vaultAccount); // Several 3rd party accounts which cannot receive funds Account alice{"alice"}; Account erin{"erin"}; // not authorized by issuer env.fund(XRP(1000), alice, erin); env(fset(alice, asfDepositAuth)); env.close(); { testcase(prefix + " fail to deposit more than assets held"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(10000)}); env(tx, ter(tecINSUFFICIENT_FUNDS)); env.close(); } { testcase(prefix + " deposit non-zero amount"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); BEAST_EXPECT( env.balance(depositor, shares) == share(50 * scale)); } { testcase(prefix + " deposit non-zero amount again"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); BEAST_EXPECT( env.balance(depositor, shares) == share(100 * scale)); } { testcase(prefix + " fail to delete non-empty vault"); auto tx = vault.del({.owner = owner, .id = keylet.key}); env(tx, ter(tecHAS_OBLIGATIONS)); env.close(); } { testcase(prefix + " fail to update because wrong owner"); auto tx = vault.set({.owner = issuer, .id = keylet.key}); tx[sfAssetsMaximum] = asset(50).number(); env(tx, ter(tecNO_PERMISSION)); env.close(); } { testcase( prefix + " fail to set maximum lower than current amount"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfAssetsMaximum] = asset(50).number(); env(tx, ter(tecLIMIT_EXCEEDED)); env.close(); } { testcase(prefix + " set maximum higher than current amount"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfAssetsMaximum] = asset(150).number(); env(tx); env.close(); } { testcase(prefix + " set maximum is idempotent, set it again"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfAssetsMaximum] = asset(150).number(); env(tx); env.close(); } { testcase(prefix + " set data"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfData] = "0"; env(tx); env.close(); } { testcase(prefix + " fail to set domain on public vault"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = to_string(base_uint<256>(42ul)); env(tx, ter{tecNO_PERMISSION}); env.close(); } { testcase(prefix + " fail to deposit more than maximum"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter(tecLIMIT_EXCEEDED)); env.close(); } { testcase(prefix + " reset maximum to zero i.e. not enforced"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfAssetsMaximum] = asset(0).number(); env(tx); env.close(); } { testcase(prefix + " fail to withdraw more than assets held"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); env(tx, ter(tecINSUFFICIENT_FUNDS)); env.close(); } { testcase(prefix + " deposit some more"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx); env.close(); BEAST_EXPECT( env.balance(depositor, shares) == share(200 * scale)); } { testcase(prefix + " clawback some"); auto code = asset.raw().native() ? ter(temMALFORMED) : ter(tesSUCCESS); auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(10)}); env(tx, code); env.close(); if (!asset.raw().native()) { BEAST_EXPECT( env.balance(depositor, shares) == share(190 * scale)); } } { testcase(prefix + " clawback all"); auto code = asset.raw().native() ? ter(tecNO_PERMISSION) : ter(tesSUCCESS); auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor}); env(tx, code); env.close(); if (!asset.raw().native()) { BEAST_EXPECT(env.balance(depositor, shares) == share(0)); { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(10)}); env(tx, ter{tecPRECISION_LOSS}); env.close(); } { auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(10)}); env(tx, ter{tecPRECISION_LOSS}); env.close(); } } } if (!asset.raw().native()) { testcase(prefix + " deposit again"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(200)}); env(tx); env.close(); BEAST_EXPECT( env.balance(depositor, shares) == share(200 * scale)); } else { testcase(prefix + " deposit/withdrawal same or less than fee"); auto const amount = env.current()->fees().base; auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = amount}); env(tx); env.close(); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = amount}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = amount}); env(tx); env.close(); // Withdraw to 3rd party tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = amount}); tx[sfDestination] = charlie.human(); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = amount - 1}); env(tx); env.close(); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = amount - 1}); env(tx); env.close(); } { testcase( prefix + " fail to withdraw to 3rd party lsfDepositAuth"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); tx[sfDestination] = alice.human(); env(tx, ter{tecNO_PERMISSION}); env.close(); } { testcase(prefix + " fail to withdraw to zero destination"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); tx[sfDestination] = "0"; env(tx, ter(temMALFORMED)); env.close(); } if (!asset.raw().native()) { testcase( prefix + " fail to withdraw to 3rd party no authorization"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); tx[sfDestination] = erin.human(); env(tx, ter{asset.raw().holds() ? tecNO_LINE : tecNO_AUTH}); env.close(); } { testcase( prefix + " fail to withdraw to 3rd party lsfRequireDestTag"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); tx[sfDestination] = dave.human(); env(tx, ter{tecDST_TAG_NEEDED}); env.close(); } { testcase(prefix + " withdraw to 3rd party lsfRequireDestTag"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); tx[sfDestination] = dave.human(); tx[sfDestinationTag] = "0"; env(tx); env.close(); } { testcase(prefix + " deposit again"); auto tx = vault.deposit( {.depositor = dave, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); } { testcase(prefix + " fail to withdraw lsfRequireDestTag"); auto tx = vault.withdraw( {.depositor = dave, .id = keylet.key, .amount = asset(50)}); env(tx, ter{tecDST_TAG_NEEDED}); env.close(); } { testcase(prefix + " withdraw with tag"); auto tx = vault.withdraw( {.depositor = dave, .id = keylet.key, .amount = asset(50)}); tx[sfDestinationTag] = "0"; env(tx); env.close(); } { testcase(prefix + " withdraw to authorized 3rd party"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); tx[sfDestination] = charlie.human(); env(tx); env.close(); BEAST_EXPECT( env.balance(depositor, shares) == share(100 * scale)); } { testcase(prefix + " withdraw to issuer"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); tx[sfDestination] = issuer.human(); env(tx); env.close(); BEAST_EXPECT( env.balance(depositor, shares) == share(50 * scale)); } if (!asset.raw().native()) { testcase(prefix + " issuer deposits"); auto tx = vault.deposit( {.depositor = issuer, .id = keylet.key, .amount = asset(10)}); env(tx); env.close(); BEAST_EXPECT(env.balance(issuer, shares) == share(10 * scale)); testcase(prefix + " issuer withdraws"); tx = vault.withdraw( {.depositor = issuer, .id = keylet.key, .amount = share(10 * scale)}); env(tx); env.close(); BEAST_EXPECT(env.balance(issuer, shares) == share(0 * scale)); } { testcase(prefix + " withdraw remaining assets"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); BEAST_EXPECT(env.balance(depositor, shares) == share(0)); if (!asset.raw().native()) { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx, ter{tecPRECISION_LOSS}); env.close(); } { auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = share(10)}); env(tx, ter{tecINSUFFICIENT_FUNDS}); env.close(); } } if (!asset.raw().native() && asset.raw().holds()) { testcase(prefix + " temporary authorization for 3rd party"); env(trust(erin, asset(1000))); env(trust(issuer, asset(0), erin, tfSetfAuth)); env(pay(issuer, erin, asset(10))); // Erin deposits all in vault, then sends shares to depositor auto tx = vault.deposit( {.depositor = erin, .id = keylet.key, .amount = asset(10)}); env(tx); env.close(); { auto tx = pay(erin, depositor, share(10 * scale)); // depositor no longer has MPToken for shares env(tx, ter{tecNO_AUTH}); env.close(); // depositor will gain MPToken for shares again env(vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1)})); env.close(); env(tx); env.close(); } testcase(prefix + " withdraw to authorized 3rd party"); // Depositor withdraws assets, destined to Erin tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(10)}); tx[sfDestination] = erin.human(); env(tx); env.close(); // Erin returns assets to issuer env(pay(erin, issuer, asset(10))); env.close(); testcase(prefix + " fail to pay to unauthorized 3rd party"); env(trust(erin, asset(0))); env.close(); // Erin has MPToken but is no longer authorized to hold assets env(pay(depositor, erin, share(1)), ter{tecNO_LINE}); env.close(); // Depositor withdraws remaining single asset tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(1)}); env(tx); env.close(); } { testcase(prefix + " fail to delete because wrong owner"); auto tx = vault.del({.owner = issuer, .id = keylet.key}); env(tx, ter(tecNO_PERMISSION)); env.close(); } { testcase(prefix + " delete empty vault"); auto tx = vault.del({.owner = owner, .id = keylet.key}); env(tx); env.close(); BEAST_EXPECT(!env.le(keylet)); } }; auto testCases = [&, this]( std::string prefix, std::function setup) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Vault vault{env}; env.fund(XRP(1000), issuer, owner, depositor, charlie, dave); env.close(); env(fset(issuer, asfAllowTrustLineClawback)); env(fset(issuer, asfRequireAuth)); env(fset(dave, asfRequireDest)); env.close(); env.require(flags(issuer, asfAllowTrustLineClawback)); env.require(flags(issuer, asfRequireAuth)); PrettyAsset asset = setup(env); testSequence(prefix, env, vault, asset); }; testCases("XRP", [&](Env& env) -> PrettyAsset { return {xrpIssue(), 1'000'000}; }); testCases("IOU", [&](Env& env) -> Asset { PrettyAsset asset = issuer["IOU"]; env(trust(owner, asset(1000))); env(trust(depositor, asset(1000))); env(trust(charlie, asset(1000))); env(trust(dave, asset(1000))); env(trust(issuer, asset(0), owner, tfSetfAuth)); env(trust(issuer, asset(0), depositor, tfSetfAuth)); env(trust(issuer, asset(0), charlie, tfSetfAuth)); env(trust(issuer, asset(0), dave, tfSetfAuth)); env(pay(issuer, depositor, asset(1000))); env.close(); return asset; }); testCases("MPT", [&](Env& env) -> Asset { MPTTester mptt{env, issuer, mptInitNoFund}; mptt.create( {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); PrettyAsset asset = mptt.issuanceID(); mptt.authorize({.account = depositor}); mptt.authorize({.account = charlie}); mptt.authorize({.account = dave}); env(pay(issuer, depositor, asset(1000))); env.close(); return asset; }); } void testPreflight() { using namespace test::jtx; struct CaseArgs { FeatureBitset features = testable_amendments() | featureSingleAssetVault; }; auto testCase = [&, this]( std::function test, CaseArgs args = {}) { Env env{*this, args.features}; Account issuer{"issuer"}; Account owner{"owner"}; Vault vault{env}; env.fund(XRP(1000), issuer, owner); env.close(); env(fset(issuer, asfAllowTrustLineClawback)); env(fset(issuer, asfRequireAuth)); env.close(); PrettyAsset asset = issuer["IOU"]; env(trust(owner, asset(1000))); env(trust(issuer, asset(0), owner, tfSetfAuth)); env(pay(issuer, owner, asset(1000))); env.close(); test(env, issuer, owner, asset, vault); }; testCase( [&](Env& env, Account const& issuer, Account const& owner, Asset const& asset, Vault& vault) { testcase("disabled single asset vault"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter{temDISABLED}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); env(tx, ter{temDISABLED}); } { auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(tx, ter{temDISABLED}); } { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(tx, ter{temDISABLED}); } { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)}); env(tx, ter{temDISABLED}); } { auto tx = vault.del({.owner = owner, .id = keylet.key}); env(tx, ter{temDISABLED}); } }, {.features = testable_amendments() - featureSingleAssetVault}); testCase([&](Env& env, Account const& issuer, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid flags"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); } { auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); } { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); } { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)}); tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); } { auto tx = vault.del({.owner = owner, .id = keylet.key}); tx[sfFlags] = tfClearDeepFreeze; env(tx, ter{temINVALID_FLAG}); } }); testCase([&](Env& env, Account const& issuer, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid fee"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[jss::Fee] = "-1"; env(tx, ter{temBAD_FEE}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[jss::Fee] = "-1"; env(tx, ter{temBAD_FEE}); } { auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[jss::Fee] = "-1"; env(tx, ter{temBAD_FEE}); } { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[jss::Fee] = "-1"; env(tx, ter{temBAD_FEE}); } { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(10)}); tx[jss::Fee] = "-1"; env(tx, ter{temBAD_FEE}); } { auto tx = vault.del({.owner = owner, .id = keylet.key}); tx[jss::Fee] = "-1"; env(tx, ter{temBAD_FEE}); } }); testCase( [&](Env& env, Account const&, Account const& owner, Asset const&, Vault& vault) { testcase("disabled permissioned domain"); auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()}); tx[sfDomainID] = to_string(base_uint<256>(42ul)); env(tx, ter{temDISABLED}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = to_string(base_uint<256>(42ul)); env(tx, ter{temDISABLED}); } { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = "0"; env(tx, ter{temDISABLED}); } }, {.features = (testable_amendments() | featureSingleAssetVault) - featurePermissionedDomains}); testCase([&](Env& env, Account const& issuer, Account const& owner, Asset const& asset, Vault& vault) { testcase("use zero vault"); auto [tx, keylet] = vault.create({.owner = owner, .asset = xrpIssue()}); { auto tx = vault.set({ .owner = owner, .id = beast::zero, }); env(tx, ter{temMALFORMED}); } { auto tx = vault.deposit( {.depositor = owner, .id = beast::zero, .amount = asset(10)}); env(tx, ter(temMALFORMED)); } { auto tx = vault.withdraw( {.depositor = owner, .id = beast::zero, .amount = asset(10)}); env(tx, ter{temMALFORMED}); } { auto tx = vault.clawback( {.issuer = issuer, .id = beast::zero, .holder = owner, .amount = asset(10)}); env(tx, ter{temMALFORMED}); } { auto tx = vault.del({ .owner = owner, .id = beast::zero, }); env(tx, ter{temMALFORMED}); } }); testCase([&](Env& env, Account const& issuer, Account const& owner, Asset const& asset, Vault& vault) { testcase("clawback from self"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = issuer, .amount = asset(10)}); env(tx, ter{temMALFORMED}); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("withdraw to bad destination"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[jss::Destination] = "0"; env(tx, ter{temMALFORMED}); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("create with Scale"); { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 255; env(tx, ter(temMALFORMED)); } { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 19; env(tx, ter(temMALFORMED)); } // accepted range from 0 to 18 { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 18; env(tx); env.close(); auto const sleVault = env.le(keylet); BEAST_EXPECT(sleVault); BEAST_EXPECT((*sleVault)[sfScale] == 18); } { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 0; env(tx); env.close(); auto const sleVault = env.le(keylet); BEAST_EXPECT(sleVault); BEAST_EXPECT((*sleVault)[sfScale] == 0); } { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); auto const sleVault = env.le(keylet); BEAST_EXPECT(sleVault); BEAST_EXPECT((*sleVault)[sfScale] == 6); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("create or set invalid data"); auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = tx1; tx[sfData] = ""; env(tx, ter(temMALFORMED)); } { auto tx = tx1; // A hexadecimal string of 257 bytes. tx[sfData] = std::string(514, 'A'); env(tx, ter(temMALFORMED)); } { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfData] = ""; env(tx, ter{temMALFORMED}); } { auto tx = vault.set({.owner = owner, .id = keylet.key}); // A hexadecimal string of 257 bytes. tx[sfData] = std::string(514, 'A'); env(tx, ter{temMALFORMED}); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("set nothing updated"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); env(tx, ter{temMALFORMED}); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("create with invalid metadata"); auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = tx1; tx[sfMPTokenMetadata] = ""; env(tx, ter(temMALFORMED)); } { auto tx = tx1; // This metadata is for the share token. // A hexadecimal string of 1025 bytes. tx[sfMPTokenMetadata] = std::string(2050, 'B'); env(tx, ter(temMALFORMED)); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("set negative maximum"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfAssetsMaximum] = negativeAmount(asset).number(); env(tx, ter{temMALFORMED}); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid deposit amount"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = negativeAmount(asset)}); env(tx, ter(temBAD_AMOUNT)); } { auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(0)}); env(tx, ter(temBAD_AMOUNT)); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid set immutable flag"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfFlags] = tfVaultPrivate; env(tx, ter(temINVALID_FLAG)); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid withdraw amount"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = negativeAmount(asset)}); env(tx, ter(temBAD_AMOUNT)); } { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(0)}); env(tx, ter(temBAD_AMOUNT)); } }); testCase([&](Env& env, Account const& issuer, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid clawback"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = vault.clawback( {.issuer = owner, .id = keylet.key, .holder = issuer, .amount = asset(50)}); env(tx, ter(temMALFORMED)); } { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = negativeAmount(asset)}); env(tx, ter(temBAD_AMOUNT)); } }); testCase([&](Env& env, Account const&, Account const& owner, Asset const& asset, Vault& vault) { testcase("invalid create"); auto [tx1, keylet] = vault.create({.owner = owner, .asset = asset}); { auto tx = tx1; tx[sfWithdrawalPolicy] = 0; env(tx, ter(temMALFORMED)); } { auto tx = tx1; tx[sfDomainID] = to_string(base_uint<256>(42ul)); env(tx, ter{temMALFORMED}); } { auto tx = tx1; tx[sfAssetsMaximum] = negativeAmount(asset).number(); env(tx, ter{temMALFORMED}); } { auto tx = tx1; tx[sfFlags] = tfVaultPrivate; tx[sfDomainID] = "0"; env(tx, ter{temMALFORMED}); } }); } // Test for non-asset specific behaviors. void testCreateFailXRP() { using namespace test::jtx; auto testCase = [this](std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; env.fund(XRP(1000), issuer, owner, depositor); env.close(); Vault vault{env}; Asset asset = xrpIssue(); test(env, issuer, owner, depositor, asset, vault); }; testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault) { testcase("nothing to set"); auto tx = vault.set({.owner = owner, .id = keylet::skip().key}); tx[sfAssetsMaximum] = asset(0).number(); env(tx, ter(tecNO_ENTRY)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault) { testcase("nothing to deposit to"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)}); env(tx, ter(tecNO_ENTRY)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault) { testcase("nothing to withdraw from"); auto tx = vault.withdraw( {.depositor = depositor, .id = keylet::skip().key, .amount = asset(10)}); env(tx, ter(tecNO_ENTRY)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { testcase("nothing to delete"); auto tx = vault.del({.owner = owner, .id = keylet::skip().key}); env(tx, ter(tecNO_ENTRY)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); testcase("transaction is good"); env(tx); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfWithdrawalPolicy] = 1; testcase("explicitly select withdrawal policy"); env(tx); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); testcase("insufficient fee"); env(tx, fee(env.current()->fees().base), ter(telINSUF_FEE_P)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); testcase("insufficient reserve"); // It is possible to construct a complicated mathematical // expression for this amount, but it is sadly not easy. env(pay(owner, issuer, XRP(775))); env.close(); env(tx, ter(tecINSUFFICIENT_RESERVE)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfFlags] = tfVaultPrivate; tx[sfDomainID] = to_string(base_uint<256>(42ul)); testcase("non-existing domain"); env(tx, ter{tecOBJECT_NOT_FOUND}); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { testcase("cannot set Scale=0"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 0; env(tx, ter{temMALFORMED}); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { testcase("cannot set Scale=1"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 1; env(tx, ter{temMALFORMED}); }); } void testCreateFailIOU() { using namespace test::jtx; { { testcase("IOU fail because MPT is disabled"); Env env{ *this, (testable_amendments() - featureMPTokensV1) | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; env.fund(XRP(1000), issuer, owner); env.close(); Vault vault{env}; Asset asset = issuer["IOU"].asset(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(temDISABLED)); env.close(); } { testcase("IOU fail create frozen"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; env.fund(XRP(1000), issuer, owner); env.close(); env(fset(issuer, asfGlobalFreeze)); env.close(); Vault vault{env}; Asset asset = issuer["IOU"].asset(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(tecFROZEN)); env.close(); } { testcase("IOU fail create no ripling"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; env.fund(XRP(1000), issuer, owner); env.close(); env(fclear(issuer, asfDefaultRipple)); env.close(); Vault vault{env}; Asset asset = issuer["IOU"].asset(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(terNO_RIPPLE)); env.close(); } { testcase("IOU no issuer"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; env.fund(XRP(1000), owner); env.close(); Vault vault{env}; Asset asset = issuer["IOU"].asset(); { auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(terNO_ACCOUNT)); env.close(); } } } { testcase("IOU fail create vault for AMM LPToken"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const gw("gateway"); Account const alice("alice"); Account const carol("carol"); IOU const USD = gw["USD"]; auto const [asset1, asset2] = std::pair(XRP(10000), USD(10000)); auto tofund = [&](STAmount const& a) -> STAmount { if (a.native()) { auto const defXRP = XRP(30000); if (a <= defXRP) return defXRP; return a + XRP(1000); } auto const defIOU = STAmount{a.issue(), 30000}; if (a <= defIOU) return defIOU; return a + STAmount{a.issue(), 1000}; }; auto const toFund1 = tofund(asset1); auto const toFund2 = tofund(asset2); BEAST_EXPECT(asset1 <= toFund1 && asset2 <= toFund2); if (!asset1.native() && !asset2.native()) fund(env, gw, {alice, carol}, {toFund1, toFund2}, Fund::All); else if (asset1.native()) fund(env, gw, {alice, carol}, toFund1, {toFund2}, Fund::All); else if (asset2.native()) fund(env, gw, {alice, carol}, toFund2, {toFund1}, Fund::All); AMM ammAlice( env, alice, asset1, asset2, CreateArg{.log = false, .tfee = 0}); Account const owner{"owner"}; env.fund(XRP(1000000), owner); Vault vault{env}; auto [tx, k] = vault.create({.owner = owner, .asset = ammAlice.lptIssue()}); env(tx, ter{tecWRONG_ASSET}); env.close(); } } void testCreateFailMPT() { using namespace test::jtx; auto testCase = [this](std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; env.fund(XRP(1000), issuer, owner, depositor); env.close(); Vault vault{env}; MPTTester mptt{env, issuer, mptInitNoFund}; // Locked because that is the default flag. mptt.create(); Asset asset = mptt.issuanceID(); test(env, issuer, owner, depositor, asset, vault); }; testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { testcase("MPT no authorization"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(tecNO_AUTH)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { testcase("MPT cannot set Scale=0"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 0; env(tx, ter{temMALFORMED}); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault) { testcase("MPT cannot set Scale=1"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 1; env(tx, ter{temMALFORMED}); }); } void testNonTransferableShares() { using namespace test::jtx; Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; env.fund(XRP(1000), issuer, owner, depositor); env.close(); Vault vault{env}; PrettyAsset asset = issuer["IOU"]; env.trust(asset(1000), owner); env(pay(issuer, owner, asset(100))); env.trust(asset(1000), depositor); env(pay(issuer, depositor, asset(100))); env.close(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfFlags] = tfVaultShareNonTransferable; env(tx); env.close(); { testcase("nontransferable deposits"); auto tx1 = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(40)}); env(tx1); auto tx2 = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(60)}); env(tx2); env.close(); } auto const vaultAccount = // [&env, key = keylet.key, this]() -> AccountID { auto jvVault = env.rpc("vault_info", strHex(key)); BEAST_EXPECT( jvVault[jss::result][jss::vault][sfAssetsTotal] == "100"); BEAST_EXPECT( jvVault[jss::result][jss::vault][jss::shares] [sfOutstandingAmount] == "100000000"); // Vault pseudo-account return parseBase58( jvVault[jss::result][jss::vault][jss::Account] .asString()) .value(); }(); auto const MptID = makeMptID(1, vaultAccount); Asset shares = MptID; { testcase("nontransferable shares cannot be moved"); env(pay(owner, depositor, shares(10)), ter{tecNO_AUTH}); env(pay(depositor, owner, shares(10)), ter{tecNO_AUTH}); } { testcase("nontransferable shares can be used to withdraw"); auto tx1 = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(20)}); env(tx1); auto tx2 = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(30)}); env(tx2); env.close(); } { testcase("nontransferable shares balance check"); auto jvVault = env.rpc("vault_info", strHex(keylet.key)); BEAST_EXPECT( jvVault[jss::result][jss::vault][sfAssetsTotal] == "50"); BEAST_EXPECT( jvVault[jss::result][jss::vault][jss::shares] [sfOutstandingAmount] == "50000000"); } { testcase("nontransferable shares withdraw rest"); auto tx1 = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(20)}); env(tx1); auto tx2 = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(30)}); env(tx2); env.close(); } { testcase("nontransferable shares delete empty vault"); auto tx = vault.del({.owner = owner, .id = keylet.key}); env(tx); BEAST_EXPECT(!env.le(keylet)); } } void testWithMPT() { using namespace test::jtx; struct CaseArgs { bool enableClawback = true; bool requireAuth = true; int initialXRP = 1000; }; auto testCase = [this]( std::function test, CaseArgs args = {}) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; env.fund(XRP(args.initialXRP), issuer, owner, depositor); env.close(); Vault vault{env}; MPTTester mptt{env, issuer, mptInitNoFund}; auto const none = LedgerSpecificFlags(0); mptt.create( {.flags = tfMPTCanTransfer | tfMPTCanLock | (args.enableClawback ? tfMPTCanClawback : none) | (args.requireAuth ? tfMPTRequireAuth : none)}); PrettyAsset asset = mptt.issuanceID(); mptt.authorize({.account = owner}); mptt.authorize({.account = depositor}); if (args.requireAuth) { mptt.authorize({.account = issuer, .holder = owner}); mptt.authorize({.account = issuer, .holder = depositor}); } env(pay(issuer, depositor, asset(1000))); env.close(); test(env, issuer, owner, depositor, asset, vault, mptt); }; testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT nothing to clawback from"); auto tx = vault.clawback( {.issuer = issuer, .id = keylet::skip().key, .holder = depositor, .amount = asset(10)}); env(tx, ter(tecNO_ENTRY)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT global lock blocks create"); mptt.set({.account = issuer, .flags = tfMPTLock}); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx, ter(tecLOCKED)); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT global lock blocks deposit"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); mptt.set({.account = issuer, .flags = tfMPTLock}); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter{tecLOCKED}); env.close(); // Can delete empty vault, even if global lock tx = vault.del({.owner = owner, .id = keylet.key}); env(tx); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT global lock blocks withdrawal"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx); env.close(); // Check that the OutstandingAmount field of MPTIssuance // accounts for the issued shares. auto v = env.le(keylet); BEAST_EXPECT(v); MPTID share = (*v)[sfShareMPTID]; auto issuance = env.le(keylet::mptIssuance(share)); BEAST_EXPECT(issuance); Number outstandingShares = issuance->at(sfOutstandingAmount); BEAST_EXPECT(outstandingShares == 100); mptt.set({.account = issuer, .flags = tfMPTLock}); env.close(); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter(tecLOCKED)); tx[sfDestination] = issuer.human(); env(tx, ter(tecLOCKED)); // Clawback is still permitted, even with global lock tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx); env.close(); // Clawback removed shares MPToken auto const mptSle = env.le(keylet::mptoken(share, depositor.id())); BEAST_EXPECT(mptSle == nullptr); // Can delete empty vault, even if global lock tx = vault.del({.owner = owner, .id = keylet.key}); env(tx); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT only issuer can clawback"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx); env.close(); { auto tx = vault.clawback( {.issuer = owner, .id = keylet.key, .holder = depositor}); env(tx, ter(tecNO_PERMISSION)); } }); testCase( [this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT depositor without MPToken, auth required"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); env(tx); env.close(); { // Remove depositor MPToken and it will not be re-created mptt.authorize( {.account = depositor, .flags = tfMPTUnauthorize}); env.close(); auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor); auto const sleMPT1 = env.le(mptoken); BEAST_EXPECT(sleMPT1 == nullptr); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter{tecNO_AUTH}); env.close(); auto const sleMPT2 = env.le(mptoken); BEAST_EXPECT(sleMPT2 == nullptr); } { // Set destination to 3rd party without MPToken Account charlie{"charlie"}; env.fund(XRP(1000), charlie); env.close(); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); tx[sfDestination] = charlie.human(); env(tx, ter(tecNO_AUTH)); } }, {.requireAuth = true}); testCase( [this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT depositor without MPToken, no auth required"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); auto v = env.le(keylet); BEAST_EXPECT(v); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); // all assets held by depositor env(tx); env.close(); { // Remove depositor's MPToken and it will be re-created mptt.authorize( {.account = depositor, .flags = tfMPTUnauthorize}); env.close(); auto const mptoken = keylet::mptoken(mptt.issuanceID(), depositor); auto const sleMPT1 = env.le(mptoken); BEAST_EXPECT(sleMPT1 == nullptr); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx); env.close(); auto const sleMPT2 = env.le(mptoken); BEAST_EXPECT(sleMPT2 != nullptr); BEAST_EXPECT(sleMPT2->at(sfMPTAmount) == 100); } { // Remove 3rd party MPToken and it will not be re-created mptt.authorize( {.account = owner, .flags = tfMPTUnauthorize}); env.close(); auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner); auto const sleMPT1 = env.le(mptoken); BEAST_EXPECT(sleMPT1 == nullptr); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); tx[sfDestination] = owner.human(); env(tx, ter(tecNO_AUTH)); env.close(); auto const sleMPT2 = env.le(mptoken); BEAST_EXPECT(sleMPT2 == nullptr); } }, {.requireAuth = false}); auto const [acctReserve, incReserve] = [this]() -> std::pair { Env env{*this, testable_amendments()}; return { env.current()->fees().accountReserve(0).drops() / DROPS_PER_XRP.drops(), env.current()->fees().increment.drops() / DROPS_PER_XRP.drops()}; }(); testCase( [&, this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT failed reserve to re-create MPToken"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); auto v = env.le(keylet); BEAST_EXPECT(v); env(pay(depositor, owner, asset(1000))); env.close(); tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(1000)}); // all assets held by owner env(tx); env.close(); { // Remove owners's MPToken and it will not be re-created mptt.authorize( {.account = owner, .flags = tfMPTUnauthorize}); env.close(); auto const mptoken = keylet::mptoken(mptt.issuanceID(), owner); auto const sleMPT = env.le(mptoken); BEAST_EXPECT(sleMPT == nullptr); // No reserve to create MPToken for asset in VaultWithdraw tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(100)}); env(tx, ter{tecINSUFFICIENT_RESERVE}); env.close(); env(pay(depositor, owner, XRP(incReserve))); env.close(); // Withdraw can now create asset MPToken, tx will succeed env(tx); env.close(); } }, {.requireAuth = false, .initialXRP = acctReserve + incReserve * 4 - 1}); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT issuance deleted"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); env(tx); env.close(); { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx); } mptt.destroy({.issuer = issuer, .id = mptt.issuanceID()}); env.close(); { auto [tx, keylet] = vault.create({.owner = depositor, .asset = asset}); env(tx, ter{tecOBJECT_NOT_FOUND}); } { auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(10)}); env(tx, ter{tecOBJECT_NOT_FOUND}); } { auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(10)}); env(tx, ter{tecOBJECT_NOT_FOUND}); } { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx, ter{tecOBJECT_NOT_FOUND}); } env(vault.del({.owner = owner, .id = keylet.key})); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT vault owner can receive shares unless unauthorized"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); env(tx); env.close(); auto const issuanceId = [&env](ripple::Keylet keylet) -> MPTID { auto const vault = env.le(keylet); return vault->at(sfShareMPTID); }(keylet); PrettyAsset shares = MPTIssue(issuanceId); { // owner has MPToken for shares they did not explicitly create env(pay(depositor, owner, shares(1))); env.close(); tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = shares(1)}); env(tx); env.close(); // owner's MPToken for vault shares not destroyed by withdraw env(pay(depositor, owner, shares(1))); env.close(); tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}); env(tx); env.close(); // owner's MPToken for vault shares not destroyed by clawback env(pay(depositor, owner, shares(1))); env.close(); // pay back, so we can destroy owner's MPToken now env(pay(owner, depositor, shares(1))); env.close(); { // explicitly destroy vault owners MPToken with zero balance Json::Value jv; jv[sfAccount] = owner.human(); jv[sfMPTokenIssuanceID] = to_string(issuanceId); jv[sfFlags] = tfMPTUnauthorize; jv[sfTransactionType] = jss::MPTokenAuthorize; env(jv); env.close(); } // owner no longer has MPToken for vault shares tx = pay(depositor, owner, shares(1)); env(tx, ter{tecNO_AUTH}); env.close(); // destroy all remaining shares, so we can delete vault tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx); env.close(); // will soft fail destroying MPToken for vault owner env(vault.del({.owner = owner, .id = keylet.key})); env.close(); } }); testCase( [this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, PrettyAsset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT clawback disabled"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); env(tx); env.close(); { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx, ter{tecNO_PERMISSION}); } }, {.enableClawback = false}); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT un-authorization"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1000)}); env(tx); env.close(); mptt.authorize( {.account = issuer, .holder = depositor, .flags = tfMPTUnauthorize}); env.close(); { auto tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter(tecNO_AUTH)); // Withdrawal to other (authorized) accounts works tx[sfDestination] = issuer.human(); env(tx); env.close(); tx[sfDestination] = owner.human(); env(tx); env.close(); } { // Cannot deposit some more auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter(tecNO_AUTH)); } // Clawback works tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(800)}); env(tx); env.close(); env(vault.del({.owner = owner, .id = keylet.key})); }); testCase([this]( Env& env, Account const& issuer, Account const& owner, Account const& depositor, Asset const& asset, Vault& vault, MPTTester& mptt) { testcase("MPT lock of vault pseudo-account"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); auto const vaultAccount = [&env, keylet = keylet, this]() -> AccountID { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); return vault->at(sfAccount); }(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx); env.close(); tx = [&]() { Json::Value jv; jv[jss::Account] = issuer.human(); jv[sfMPTokenIssuanceID] = to_string(asset.get().getMptID()); jv[jss::Holder] = toBase58(vaultAccount); jv[jss::TransactionType] = jss::MPTokenIssuanceSet; jv[jss::Flags] = tfMPTLock; return jv; }(); env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter(tecLOCKED)); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(100)}); env(tx, ter(tecLOCKED)); // Clawback works, even when locked tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(100)}); env(tx); // Can delete an empty vault even when asset is locked. tx = vault.del({.owner = owner, .id = keylet.key}); env(tx); }); { testcase("MPT shares to a vault"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account owner{"owner"}; Account issuer{"issuer"}; env.fund(XRP(1000000), owner, issuer); env.close(); Vault vault{env}; MPTTester mptt{env, issuer, mptInitNoFund}; mptt.create( {.flags = tfMPTCanTransfer | tfMPTCanLock | lsfMPTCanClawback | tfMPTRequireAuth}); mptt.authorize({.account = owner}); mptt.authorize({.account = issuer, .holder = owner}); PrettyAsset asset = mptt.issuanceID(); env(pay(issuer, owner, asset(100))); auto [tx1, k1] = vault.create({.owner = owner, .asset = asset}); env(tx1); env.close(); auto const shares = [&env, keylet = k1, this]() -> Asset { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); return MPTIssue(vault->at(sfShareMPTID)); }(); auto [tx2, k2] = vault.create({.owner = owner, .asset = shares}); env(tx2, ter{tecWRONG_ASSET}); env.close(); } } void testWithIOU() { using namespace test::jtx; struct CaseArgs { int initialXRP = 1000; Number initialIOU = 200; double transferRate = 1.0; }; auto testCase = [&, this]( std::function vaultAccount, Vault& vault, PrettyAsset const& asset, std::function issuanceId)> test, CaseArgs args = {}) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Account const issuer{"issuer"}; Account const charlie{"charlie"}; Vault vault{env}; env.fund(XRP(args.initialXRP), issuer, owner, charlie); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); PrettyAsset const asset = issuer["IOU"]; env.trust(asset(1000), owner); env.trust(asset(1000), charlie); env(pay(issuer, owner, asset(args.initialIOU))); env(rate(issuer, args.transferRate)); env.close(); auto const vaultAccount = [&env](ripple::Keylet keylet) -> Account { return Account("vault", env.le(keylet)->at(sfAccount)); }; auto const issuanceId = [&env](ripple::Keylet keylet) -> MPTID { return env.le(keylet)->at(sfShareMPTID); }; test( env, owner, issuer, charlie, vaultAccount, vault, asset, issuanceId); }; testCase([&, this]( Env& env, Account const& owner, Account const& issuer, Account const&, auto vaultAccount, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU cannot use different asset"); PrettyAsset const foo = issuer["FOO"]; auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); { // Cannot create new trustline to a vault auto tx = [&, account = vaultAccount(keylet)]() { Json::Value jv; jv[jss::Account] = issuer.human(); { auto& ja = jv[jss::LimitAmount] = foo(0).value().getJson(JsonOptions::none); ja[jss::issuer] = toBase58(account); } jv[jss::TransactionType] = jss::TrustSet; jv[jss::Flags] = tfSetFreeze; return jv; }(); env(tx, ter{tecNO_PERMISSION}); env.close(); } { auto tx = vault.deposit( {.depositor = issuer, .id = keylet.key, .amount = foo(20)}); env(tx, ter{tecWRONG_ASSET}); env.close(); } { auto tx = vault.withdraw( {.depositor = issuer, .id = keylet.key, .amount = foo(20)}); env(tx, ter{tecWRONG_ASSET}); env.close(); } env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }); testCase([&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto vaultAccount, Vault& vault, PrettyAsset const& asset, auto issuanceId) { testcase("IOU frozen trust line to vault account"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(100)})); env.close(); Asset const share = Asset(issuanceId(keylet)); // Freeze the trustline to the vault auto trustSet = [&, account = vaultAccount(keylet)]() { Json::Value jv; jv[jss::Account] = issuer.human(); { auto& ja = jv[jss::LimitAmount] = asset(0).value().getJson(JsonOptions::none); ja[jss::issuer] = toBase58(account); } jv[jss::TransactionType] = jss::TrustSet; jv[jss::Flags] = tfSetFreeze; return jv; }(); env(trustSet); env.close(); { // Note, the "frozen" state of the trust line to vault account // is reported as "locked" state of the vault shares, because // this state is attached to shares by means of the transitive // isFrozen. auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(80)}); env(tx, ter{tecLOCKED}); } { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(100)}); env(tx, ter{tecLOCKED}); // also when trying to withdraw to a 3rd party tx[sfDestination] = charlie.human(); env(tx, ter{tecLOCKED}); env.close(); } { // Clawback works, even when locked auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)}); env(tx); env.close(); } // Clear the frozen state trustSet[jss::Flags] = tfClearFreeze; env(trustSet); env.close(); env(vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = share(50'000'000)})); env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }); testCase( [&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto vaultAccount, Vault& vault, PrettyAsset const& asset, auto issuanceId) { testcase("IOU transfer fees not applied"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(100)})); env.close(); auto const issue = asset.raw().get(); Asset const share = Asset(issuanceId(keylet)); // transfer fees ignored on deposit BEAST_EXPECT(env.balance(owner, issue) == asset(100)); BEAST_EXPECT( env.balance(vaultAccount(keylet), issue) == asset(100)); { auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(50)}); env(tx); env.close(); } // transfer fees ignored on clawback BEAST_EXPECT(env.balance(owner, issue) == asset(100)); BEAST_EXPECT( env.balance(vaultAccount(keylet), issue) == asset(50)); env(vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = share(20'000'000)})); // transfer fees ignored on withdraw BEAST_EXPECT(env.balance(owner, issue) == asset(120)); BEAST_EXPECT( env.balance(vaultAccount(keylet), issue) == asset(30)); { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = share(30'000'000)}); tx[sfDestination] = charlie.human(); env(tx); } // transfer fees ignored on withdraw to 3rd party BEAST_EXPECT(env.balance(owner, issue) == asset(120)); BEAST_EXPECT(env.balance(charlie, issue) == asset(30)); BEAST_EXPECT( env.balance(vaultAccount(keylet), issue) == asset(0)); env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }, CaseArgs{.transferRate = 1.25}); testCase([&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU frozen trust line to depositor"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(100)})); env.close(); // Withdraw to 3rd party works auto const withdrawToCharlie = [&](ripple::Keylet keylet) { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[sfDestination] = charlie.human(); return tx; }(keylet); env(withdrawToCharlie); // Freeze the owner env(trust(issuer, asset(0), owner, tfSetFreeze)); env.close(); // Cannot withdraw auto const withdraw = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(withdraw, ter{tecFROZEN}); // Cannot withdraw to 3rd party env(withdrawToCharlie, ter{tecLOCKED}); env.close(); { // Cannot deposit some more auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(tx, ter{tecFROZEN}); } { // Clawback still works auto tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}); env(tx); env.close(); } env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }); testCase([&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU no trust line to 3rd party"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(100)})); env.close(); Account const erin{"erin"}; env.fund(XRP(1000), erin); env.close(); // Withdraw to 3rd party without trust line auto const tx1 = [&](ripple::Keylet keylet) { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[sfDestination] = erin.human(); return tx; }(keylet); env(tx1, ter{tecNO_LINE}); }); testCase([&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU no trust line to depositor"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); // reset limit, so deposit of all funds will delete the trust line env.trust(asset(0), owner); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(200)})); env.close(); auto trustline = env.le(keylet::line(owner, asset.raw().get())); BEAST_EXPECT(trustline == nullptr); // Withdraw without trust line, will succeed auto const tx1 = [&](ripple::Keylet keylet) { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); return tx; }(keylet); env(tx1); }); testCase( [&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto const& vaultAccount, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU calculation rounding"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = 1; env(tx); env.close(); auto const startingOwnerBalance = env.balance(owner, asset); BEAST_EXPECT( (startingOwnerBalance.value() == STAmount{asset, 11875, -2})); // This operation (first deposit 100, then 3.75 x 5) is known to // have triggered calculation rounding errors in Number // (addition and division), causing the last deposit to be // blocked by Vault invariants. env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(100)})); auto const tx1 = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(Number(375, -2))}); for (auto i = 0; i < 5; ++i) { env(tx1); } env.close(); { STAmount const xfer{asset, 1185, -1}; BEAST_EXPECT( env.balance(owner, asset) == startingOwnerBalance.value() - xfer); BEAST_EXPECT( env.balance(vaultAccount(keylet), asset) == xfer); auto const vault = env.le(keylet); BEAST_EXPECT(vault->at(sfAssetsAvailable) == xfer); BEAST_EXPECT(vault->at(sfAssetsTotal) == xfer); } // Total vault balance should be 118.5 IOU. Withdraw and delete // the vault to verify this exact amount was deposited and the // owner has matching shares env(vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(Number(1000 + 37 * 5, -1))})); { BEAST_EXPECT( env.balance(owner, asset) == startingOwnerBalance.value()); BEAST_EXPECT( env.balance(vaultAccount(keylet), asset) == beast::zero); auto const vault = env.le(keylet); BEAST_EXPECT(vault->at(sfAssetsAvailable) == beast::zero); BEAST_EXPECT(vault->at(sfAssetsTotal) == beast::zero); } env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }, {.initialIOU = Number(11875, -2)}); auto const [acctReserve, incReserve] = [this]() -> std::pair { Env env{*this, testable_amendments()}; return { env.current()->fees().accountReserve(0).drops() / DROPS_PER_XRP.drops(), env.current()->fees().increment.drops() / DROPS_PER_XRP.drops()}; }(); testCase( [&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU no trust line to depositor no reserve"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); // reset limit, so deposit of all funds will delete the trust // line env.trust(asset(0), owner); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(200)})); env.close(); auto trustline = env.le(keylet::line(owner, asset.raw().get())); BEAST_EXPECT(trustline == nullptr); // Fail because not enough reserve to create trust line tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(tx, ter{tecNO_LINE_INSUF_RESERVE}); env.close(); env(pay(charlie, owner, XRP(incReserve))); env.close(); // Withdraw can now create trust line, will succeed env(tx); env.close(); }, CaseArgs{.initialXRP = acctReserve + incReserve * 4 - 1}); testCase( [&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU no reserve for share MPToken"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); env(pay(owner, charlie, asset(100))); env.close(); // Use up some reserve on tickets env(ticket::create(charlie, 2)); env.close(); // Fail because not enough reserve to create MPToken for shares tx = vault.deposit( {.depositor = charlie, .id = keylet.key, .amount = asset(100)}); env(tx, ter{tecINSUFFICIENT_RESERVE}); env.close(); env(pay(issuer, charlie, XRP(incReserve))); env.close(); // Deposit can now create MPToken, will succeed env(tx); env.close(); }, CaseArgs{.initialXRP = acctReserve + incReserve * 4 - 1}); testCase([&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU frozen trust line to 3rd party"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(100)})); env.close(); // Withdraw to 3rd party works auto const withdrawToCharlie = [&](ripple::Keylet keylet) { auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); tx[sfDestination] = charlie.human(); return tx; }(keylet); env(withdrawToCharlie); // Freeze the 3rd party env(trust(issuer, asset(0), charlie, tfSetFreeze)); env.close(); // Can withdraw auto const withdraw = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(withdraw); env.close(); // Cannot withdraw to 3rd party env(withdrawToCharlie, ter{tecFROZEN}); env.close(); env(vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)})); env.close(); env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }); testCase([&, this]( Env& env, Account const& owner, Account const& issuer, Account const& charlie, auto, Vault& vault, PrettyAsset const& asset, auto&&...) { testcase("IOU global freeze"); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); env(vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(100)})); env.close(); env(fset(issuer, asfGlobalFreeze)); env.close(); { // Cannot withdraw auto tx = vault.withdraw( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(tx, ter{tecFROZEN}); // Cannot withdraw to 3rd party tx[sfDestination] = charlie.human(); env(tx, ter{tecFROZEN}); env.close(); // Cannot deposit some more tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(10)}); env(tx, ter{tecFROZEN}); } // Clawback is permitted env(vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)})); env.close(); env(vault.del({.owner = owner, .id = keylet.key})); env.close(); }); } void testWithDomainCheck() { using namespace test::jtx; testcase("private vault"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; Account depositor{"depositor"}; Account charlie{"charlie"}; Account pdOwner{"pdOwner"}; Account credIssuer1{"credIssuer1"}; Account credIssuer2{"credIssuer2"}; std::string const credType = "credential"; Vault vault{env}; env.fund( XRP(1000), issuer, owner, depositor, charlie, pdOwner, credIssuer1, credIssuer2); env.close(); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); env.require(flags(issuer, asfAllowTrustLineClawback)); PrettyAsset asset = issuer["IOU"]; env.trust(asset(1000), owner); env(pay(issuer, owner, asset(500))); env.trust(asset(1000), depositor); env(pay(issuer, depositor, asset(500))); env.trust(asset(1000), charlie); env(pay(issuer, charlie, asset(5))); env.close(); auto [tx, keylet] = vault.create( {.owner = owner, .asset = asset, .flags = tfVaultPrivate}); env(tx); env.close(); BEAST_EXPECT(env.le(keylet)); { testcase("private vault owner can deposit"); auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(50)}); env(tx); } { testcase("private vault depositor not authorized yet"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx, ter{tecNO_AUTH}); } { testcase("private vault cannot set non-existing domain"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = to_string(base_uint<256>(42ul)); env(tx, ter{tecOBJECT_NOT_FOUND}); } { testcase("private vault set domainId"); { pdomain::Credentials const credentials1{ {.issuer = credIssuer1, .credType = credType}}; env(pdomain::setTx(pdOwner, credentials1)); auto const domainId1 = [&]() { auto tx = env.tx()->getJson(JsonOptions::none); return pdomain::getNewDomain(env.meta()); }(); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = to_string(domainId1); env(tx); env.close(); // Update domain second time, should be harmless env(tx); env.close(); } { pdomain::Credentials const credentials{ {.issuer = credIssuer1, .credType = credType}, {.issuer = credIssuer2, .credType = credType}}; env(pdomain::setTx(pdOwner, credentials)); auto const domainId = [&]() { auto tx = env.tx()->getJson(JsonOptions::none); return pdomain::getNewDomain(env.meta()); }(); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = to_string(domainId); env(tx); env.close(); // Should be idempotent tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = to_string(domainId); env(tx); env.close(); } } { testcase("private vault depositor still not authorized"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx, ter{tecNO_AUTH}); env.close(); } auto const credKeylet = credentials::keylet(depositor, credIssuer1, credType); { testcase("private vault depositor now authorized"); env(credentials::create(depositor, credIssuer1, credType)); env(credentials::accept(depositor, credIssuer1, credType)); env(credentials::create(charlie, credIssuer1, credType)); // charlie's credential not accepted env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle != nullptr); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); tx = vault.deposit( {.depositor = charlie, .id = keylet.key, .amount = asset(50)}); env(tx, ter{tecNO_AUTH}); env.close(); } { testcase("private vault depositor lost authorization"); env(credentials::deleteCred( credIssuer1, depositor, credIssuer1, credType)); env(credentials::deleteCred( credIssuer1, charlie, credIssuer1, credType)); env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle == nullptr); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx, ter{tecNO_AUTH}); env.close(); } auto const shares = [&env, keylet = keylet, this]() -> Asset { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); return MPTIssue(vault->at(sfShareMPTID)); }(); { testcase("private vault expired authorization"); uint32_t const closeTime = env.current() ->info() .parentCloseTime.time_since_epoch() .count(); { auto tx0 = credentials::create(depositor, credIssuer2, credType); tx0[sfExpiration] = closeTime + 20; env(tx0); tx0 = credentials::create(charlie, credIssuer2, credType); tx0[sfExpiration] = closeTime + 20; env(tx0); env.close(); env(credentials::accept(depositor, credIssuer2, credType)); env(credentials::accept(charlie, credIssuer2, credType)); env.close(); } { auto tx1 = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx1); env.close(); auto const tokenKeylet = keylet::mptoken( shares.get().getMptID(), depositor.id()); BEAST_EXPECT(env.le(tokenKeylet) != nullptr); } { // time advance env.close(); env.close(); env.close(); auto const credsKeylet = credentials::keylet(depositor, credIssuer2, credType); BEAST_EXPECT(env.le(credsKeylet) != nullptr); auto tx2 = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(1)}); env(tx2, ter{tecEXPIRED}); env.close(); BEAST_EXPECT(env.le(credsKeylet) == nullptr); } { auto const credsKeylet = credentials::keylet(charlie, credIssuer2, credType); BEAST_EXPECT(env.le(credsKeylet) != nullptr); auto const tokenKeylet = keylet::mptoken( shares.get().getMptID(), charlie.id()); BEAST_EXPECT(env.le(tokenKeylet) == nullptr); auto tx3 = vault.deposit( {.depositor = charlie, .id = keylet.key, .amount = asset(2)}); env(tx3, ter{tecEXPIRED}); env.close(); BEAST_EXPECT(env.le(credsKeylet) == nullptr); BEAST_EXPECT(env.le(tokenKeylet) == nullptr); } } { testcase("private vault reset domainId"); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = "0"; env(tx); env.close(); tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx, ter{tecNO_AUTH}); env.close(); tx = vault.withdraw( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = depositor, .amount = asset(0)}); env(tx); tx = vault.clawback( {.issuer = issuer, .id = keylet.key, .holder = owner, .amount = asset(0)}); env(tx); env.close(); tx = vault.del({ .owner = owner, .id = keylet.key, }); env(tx); } } void testWithDomainCheckXRP() { using namespace test::jtx; testcase("private XRP vault"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account owner{"owner"}; Account depositor{"depositor"}; Account alice{"charlie"}; std::string const credType = "credential"; Vault vault{env}; env.fund(XRP(100000), owner, depositor, alice); env.close(); PrettyAsset asset = xrpIssue(); auto [tx, keylet] = vault.create( {.owner = owner, .asset = asset, .flags = tfVaultPrivate}); env(tx); env.close(); auto const [vaultAccount, issuanceId] = [&env, keylet = keylet, this]() -> std::tuple { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); return {vault->at(sfAccount), vault->at(sfShareMPTID)}; }(); BEAST_EXPECT(env.le(keylet::account(vaultAccount))); BEAST_EXPECT(env.le(keylet::mptIssuance(issuanceId))); PrettyAsset shares{issuanceId}; { testcase("private XRP vault owner can deposit"); auto tx = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); } { testcase("private XRP vault cannot pay shares to depositor yet"); env(pay(owner, depositor, shares(1)), ter{tecNO_AUTH}); } { testcase("private XRP vault depositor not authorized yet"); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx, ter{tecNO_AUTH}); } { testcase("private XRP vault set DomainID"); pdomain::Credentials const credentials{ {.issuer = owner, .credType = credType}}; env(pdomain::setTx(owner, credentials)); auto const domainId = [&]() { auto tx = env.tx()->getJson(JsonOptions::none); return pdomain::getNewDomain(env.meta()); }(); auto tx = vault.set({.owner = owner, .id = keylet.key}); tx[sfDomainID] = to_string(domainId); env(tx); env.close(); } auto const credKeylet = credentials::keylet(depositor, owner, credType); { testcase("private XRP vault depositor now authorized"); env(credentials::create(depositor, owner, credType)); env(credentials::accept(depositor, owner, credType)); env.close(); BEAST_EXPECT(env.le(credKeylet)); auto tx = vault.deposit( {.depositor = depositor, .id = keylet.key, .amount = asset(50)}); env(tx); env.close(); } { testcase("private XRP vault can pay shares to depositor"); env(pay(owner, depositor, shares(1))); } { testcase("private XRP vault cannot pay shares to 3rd party"); Json::Value jv; jv[sfAccount] = alice.human(); jv[sfTransactionType] = jss::MPTokenAuthorize; jv[sfMPTokenIssuanceID] = to_string(issuanceId); env(jv); env.close(); env(pay(owner, alice, shares(1)), ter{tecNO_AUTH}); } } void testFailedPseudoAccount() { using namespace test::jtx; testcase("fail pseudo-account allocation"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Vault vault{env}; env.fund(XRP(1000), owner); auto const keylet = keylet::vault(owner.id(), env.seq(owner)); for (int i = 0; i < 256; ++i) { AccountID const accountId = ripple::pseudoAccountAddress(*env.current(), keylet.key); env(pay(env.master.id(), accountId, XRP(1000)), seq(autofill), fee(autofill), sig(autofill)); } auto [tx, keylet1] = vault.create({.owner = owner, .asset = xrpIssue()}); BEAST_EXPECT(keylet.key == keylet1.key); env(tx, ter{terADDRESS_COLLISION}); } void testScaleIOU() { using namespace test::jtx; struct Data { Account const& owner; Account const& issuer; Account const& depositor; Account const& vaultAccount; MPTIssue shares; PrettyAsset const& share; Vault& vault; ripple::Keylet keylet; Issue assets; PrettyAsset const& asset; std::function)> peek; }; auto testCase = [&, this]( std::uint8_t scale, std::function test) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Account const issuer{"issuer"}; Account const depositor{"depositor"}; Vault vault{env}; env.fund(XRP(1000), issuer, owner, depositor); env(fset(issuer, asfAllowTrustLineClawback)); env.close(); PrettyAsset const asset = issuer["IOU"]; env.trust(asset(1000), owner); env.trust(asset(1000), depositor); env(pay(issuer, owner, asset(200))); env(pay(issuer, depositor, asset(200))); env.close(); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); tx[sfScale] = scale; env(tx); auto const [vaultAccount, issuanceId] = [&env](ripple::Keylet keylet) -> std::tuple { auto const vault = env.le(keylet); return { Account("vault", vault->at(sfAccount)), vault->at(sfShareMPTID)}; }(keylet); MPTIssue shares(issuanceId); env.memoize(vaultAccount); auto const peek = [=, &env, this](std::function fn) -> bool { return env.app().openLedger().modify( [&](OpenView& view, beast::Journal j) -> bool { Sandbox sb(&view, tapNONE); auto vault = sb.peek(keylet::vault(keylet.key)); if (!BEAST_EXPECT(vault != nullptr)) return false; auto shares = sb.peek( keylet::mptIssuance(vault->at(sfShareMPTID))); if (!BEAST_EXPECT(shares != nullptr)) return false; if (fn(*vault, *shares)) { sb.update(vault); sb.update(shares); sb.apply(view); return true; } return false; }); }; test( env, {.owner = owner, .issuer = issuer, .depositor = depositor, .vaultAccount = vaultAccount, .shares = shares, .share = PrettyAsset(shares), .vault = vault, .keylet = keylet, .assets = asset.raw().get(), .asset = asset, .peek = peek}); }; testCase(18, [&, this](Env& env, Data d) { testcase("Scale deposit overflow on first deposit"); auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)}); env(tx, ter{tecPATH_DRY}); env.close(); }); testCase(18, [&, this](Env& env, Data d) { testcase("Scale deposit overflow on second deposit"); { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); env(tx); env.close(); } { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(10)}); env(tx, ter{tecPATH_DRY}); env.close(); } }); testCase(18, [&, this](Env& env, Data d) { testcase("Scale deposit overflow on total shares"); { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); env(tx); env.close(); } { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); env(tx, ter{tecPATH_DRY}); env.close(); } }); testCase(1, [&, this](Env& env, Data d) { testcase("Scale deposit exact"); auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(1)}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(10)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - 1)); }); testCase(1, [&, this](Env& env, Data d) { testcase("Scale deposit insignificant amount"); auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(9, -2))}); env(tx, ter{tecPRECISION_LOSS}); }); testCase(1, [&, this](Env& env, Data d) { testcase("Scale deposit exact, using full precision"); auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(15, -1))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(15)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(15, -1))); }); testCase(1, [&, this](Env& env, Data d) { testcase("Scale deposit exact, truncating from .5"); auto const start = env.balance(d.depositor, d.assets).number(); // Each of the cases below will transfer exactly 1.2 IOU to the // vault and receive 12 shares in exchange { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(125, -2))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1))); } { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(1201, -3))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(24)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(24, -1))); } { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(1299, -3))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(36)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(36, -1))); } }); testCase(1, [&, this](Env& env, Data d) { testcase("Scale deposit exact, truncating from .01"); auto const start = env.balance(d.depositor, d.assets).number(); // round to 12 auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(1201, -3))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1))); { // round to 6 auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(69, -2))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(18, -1))); } }); testCase(1, [&, this](Env& env, Data d) { testcase("Scale deposit exact, truncating from .99"); auto const start = env.balance(d.depositor, d.assets).number(); // round to 12 auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(1299, -3))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(12)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(12, -1))); { // round to 6 auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(62, -2))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(18)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(18, -1))); } }); testCase(1, [&, this](Env& env, Data d) { // initial setup: deposit 100 IOU, receive 1000 shares auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(100, 0))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0))); { testcase("Scale redeem exact"); // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 100 * 100 / 1000 = 100 * 0.1 = 10 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.share, Number(100, 0))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(900)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0))); } { testcase("Scale redeem with rounding"); // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5 auto const start = env.balance(d.depositor, d.assets).number(); d.peek([](SLE& vault, auto&) -> bool { vault[sfAssetsAvailable] = Number(1); return true; }); // Note, this transaction fails first (because of above change // in the open ledger) but then succeeds when the ledger is // closed (because a modification like above is not persistent), // which is why the checks below are expected to pass. auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.share, Number(25, 0))}); env(tx, ter{tecINSUFFICIENT_FUNDS}); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(900 - 25)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(900 - 25, 0))); } { testcase("Scale redeem exact"); // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 87.5 * 21 / 875 = 87.5 * 0.024 = 2.1 auto const start = env.balance(d.depositor, d.assets).number(); tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.share, Number(21, 0))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(875 - 21)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(21, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(875 - 21, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(875 - 21, 0))); } { testcase("Scale redeem rest"); auto const rest = env.balance(d.depositor, d.shares).number(); tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.share, rest)}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets).number() == 0); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares).number() == 0); } }); testCase(18, [&, this](Env& env, Data d) { testcase("Scale withdraw overflow"); { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); env(tx); env.close(); } { auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(10, 0))}); env(tx, ter{tecPATH_DRY}); env.close(); } }); testCase(1, [&, this](Env& env, Data d) { // initial setup: deposit 100 IOU, receive 1000 shares auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(100, 0))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-1000, 0))); { testcase("Scale withdraw exact"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 1000 * 10 / 100 = 1000 * 0.1 = 100 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 100 * 100 / 1000 = 100 * 0.1 = 10 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(10, 0))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(900)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(10, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, Number(-900, 0))); } { testcase("Scale withdraw insignificant amount"); auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(4, -2))}); env(tx, ter{tecPRECISION_LOSS}); } { testcase("Scale withdraw with rounding assets"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5 auto const start = env.balance(d.depositor, d.assets).number(); d.peek([](SLE& vault, auto&) -> bool { vault[sfAssetsAvailable] = Number(1); return true; }); // Note, this transaction fails first (because of above change // in the open ledger) but then succeeds when the ledger is // closed (because a modification like above is not persistent), // which is why the checks below are expected to pass. auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(25, -1))}); env(tx, ter{tecINSUFFICIENT_FUNDS}); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(900 - 25)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(900 - 25, 0))); } { testcase("Scale withdraw with rounding shares up"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 875 * 3.75 / 87.5 = 875 * 0.042857... = 37.5 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 87.5 * 38 / 875 = 87.5 * 0.043428... = 3.8 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(375, -2))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(875 - 38)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(38, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(875 - 38, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(875 - 38, 0))); } { testcase("Scale withdraw with rounding shares down"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 837 * 3.72 / 83.7 = 837 * 0.04444... = 37.2 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 83.7 * 37 / 837 = 83.7 * 0.044205... = 3.7 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(372, -2))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(837 - 37)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(37, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(837 - 37, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(837 - 37, 0))); } { testcase("Scale withdraw tiny amount"); auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(9, -2))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(800 - 1)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start + Number(1, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(800 - 1, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(800 - 1, 0))); } { testcase("Scale withdraw rest"); auto const rest = env.balance(d.vaultAccount, d.assets).number(); tx = d.vault.withdraw( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, rest)}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets).number() == 0); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares).number() == 0); } }); testCase(18, [&, this](Env& env, Data d) { testcase("Scale clawback overflow"); { auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = d.asset(5)}); env(tx); env.close(); } { auto tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, Number(10, 0))}); env(tx, ter{tecPATH_DRY}); env.close(); } }); testCase(1, [&, this](Env& env, Data d) { // initial setup: deposit 100 IOU, receive 1000 shares auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.deposit( {.depositor = d.depositor, .id = d.keylet.key, .amount = STAmount(d.asset, Number(100, 0))}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares) == d.share(1000)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start - Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(100, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(1000, 0))); { testcase("Scale clawback exact"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 1000 * 10 / 100 = 1000 * 0.1 = 100 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 100 * 100 / 1000 = 100 * 0.1 = 10 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, Number(10, 0))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(900)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start)); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(90, 0))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(900, 0))); } { testcase("Scale clawback insignificant amount"); auto tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, Number(4, -2))}); env(tx, ter{tecPRECISION_LOSS}); } { testcase("Scale clawback with rounding assets"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 900 * 2.5 / 90 = 900 * 0.02777... = 25 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 90 * 25 / 900 = 90 * 0.02777... = 2.5 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, Number(25, -1))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(900 - 25)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start)); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(900 - 25, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(900 - 25, 0))); } { testcase("Scale clawback with rounding shares up"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 875 * 3.75 / 87.5 = 875 * 0.042857... = 37.5 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 87.5 * 38 / 875 = 87.5 * 0.043428... = 3.8 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, Number(375, -2))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(875 - 38)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start)); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(875 - 38, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(875 - 38, 0))); } { testcase("Scale clawback with rounding shares down"); // assetsToSharesWithdraw: // shares = sharesTotal * (assets / assetsTotal) // shares = 837 * 3.72 / 83.7 = 837 * 0.04444... = 37.2 // sharesToAssetsWithdraw: // assets = assetsTotal * (shares / sharesTotal) // assets = 83.7 * 37 / 837 = 83.7 * 0.044205... = 3.7 auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, Number(372, -2))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(837 - 37)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start)); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(837 - 37, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(837 - 37, 0))); } { testcase("Scale clawback tiny amount"); auto const start = env.balance(d.depositor, d.assets).number(); auto tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, Number(9, -2))}); env(tx); env.close(); BEAST_EXPECT( env.balance(d.depositor, d.shares) == d.share(800 - 1)); BEAST_EXPECT( env.balance(d.depositor, d.assets) == STAmount(d.asset, start)); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets) == STAmount(d.asset, Number(800 - 1, -1))); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares) == STAmount(d.share, -Number(800 - 1, 0))); } { testcase("Scale clawback rest"); auto const rest = env.balance(d.vaultAccount, d.assets).number(); d.peek([](SLE& vault, auto&) -> bool { vault[sfAssetsAvailable] = Number(5); return true; }); // Note, this transaction yields two different results: // * in the open ledger, with AssetsAvailable = 5 // * when the ledger is closed with unmodified AssetsAvailable // because a modification like above is not persistent. tx = d.vault.clawback( {.issuer = d.issuer, .id = d.keylet.key, .holder = d.depositor, .amount = STAmount(d.asset, rest)}); env(tx); env.close(); BEAST_EXPECT(env.balance(d.depositor, d.shares).number() == 0); BEAST_EXPECT( env.balance(d.vaultAccount, d.assets).number() == 0); BEAST_EXPECT( env.balance(d.vaultAccount, d.shares).number() == 0); } }); } void testRPC() { using namespace test::jtx; testcase("RPC"); Env env{*this, testable_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; Account const issuer{"issuer"}; Vault vault{env}; env.fund(XRP(1000), issuer, owner); env.close(); PrettyAsset asset = issuer["IOU"]; env.trust(asset(1000), owner); env(pay(issuer, owner, asset(200))); env.close(); auto const sequence = env.seq(owner); auto [tx, keylet] = vault.create({.owner = owner, .asset = asset}); env(tx); env.close(); // Set some fields { auto tx1 = vault.deposit( {.depositor = owner, .id = keylet.key, .amount = asset(50)}); env(tx1); auto tx2 = vault.set({.owner = owner, .id = keylet.key}); tx2[sfAssetsMaximum] = asset(1000).number(); env(tx2); env.close(); } auto const sleVault = [&env, keylet = keylet, this]() { auto const vault = env.le(keylet); BEAST_EXPECT(vault != nullptr); return vault; }(); auto const check = [&, keylet = keylet, sle = sleVault, this]( Json::Value const& vault, Json::Value const& issuance = Json::nullValue) { BEAST_EXPECT(vault.isObject()); constexpr auto checkString = [](auto& node, SField const& field, std::string v) -> bool { return node.isMember(field.fieldName) && node[field.fieldName].isString() && node[field.fieldName] == v; }; constexpr auto checkObject = [](auto& node, SField const& field, Json::Value v) -> bool { return node.isMember(field.fieldName) && node[field.fieldName].isObject() && node[field.fieldName] == v; }; constexpr auto checkInt = [](auto& node, SField const& field, int v) -> bool { return node.isMember(field.fieldName) && ((node[field.fieldName].isInt() && node[field.fieldName] == Json::Int(v)) || (node[field.fieldName].isUInt() && node[field.fieldName] == Json::UInt(v))); }; BEAST_EXPECT(vault["LedgerEntryType"].asString() == "Vault"); BEAST_EXPECT(vault[jss::index].asString() == strHex(keylet.key)); BEAST_EXPECT(checkInt(vault, sfFlags, 0)); // Ignore all other standard fields, this test doesn't care BEAST_EXPECT( checkString(vault, sfAccount, toBase58(sle->at(sfAccount)))); BEAST_EXPECT( checkObject(vault, sfAsset, to_json(sle->at(sfAsset)))); BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50")); BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000")); BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50")); BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0")); auto const strShareID = strHex(sle->at(sfShareMPTID)); BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID)); BEAST_EXPECT(checkString(vault, sfOwner, toBase58(owner.id()))); BEAST_EXPECT(checkInt(vault, sfSequence, sequence)); BEAST_EXPECT(checkInt( vault, sfWithdrawalPolicy, vaultStrategyFirstComeFirstServe)); if (issuance.isObject()) { BEAST_EXPECT( issuance["LedgerEntryType"].asString() == "MPTokenIssuance"); BEAST_EXPECT( issuance[jss::mpt_issuance_id].asString() == strShareID); BEAST_EXPECT(checkInt(issuance, sfSequence, 1)); BEAST_EXPECT(checkInt( issuance, sfFlags, int(lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer))); BEAST_EXPECT( checkString(issuance, sfOutstandingAmount, "50000000")); } }; { testcase("RPC ledger_entry selected by key"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault] = strHex(keylet.key); auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error)); BEAST_EXPECT(jvVault[jss::result].isMember(jss::node)); check(jvVault[jss::result][jss::node]); } { testcase("RPC ledger_entry selected by owner and seq"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault][jss::owner] = owner.human(); jvParams[jss::vault][jss::seq] = sequence; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT(!jvVault[jss::result].isMember(jss::error)); BEAST_EXPECT(jvVault[jss::result].isMember(jss::node)); check(jvVault[jss::result][jss::node]); } { testcase("RPC ledger_entry cannot find vault by key"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault] = to_string(uint256(42)); auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "entryNotFound"); } { testcase("RPC ledger_entry cannot find vault by owner and seq"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault][jss::owner] = issuer.human(); jvParams[jss::vault][jss::seq] = 1'000'000; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "entryNotFound"); } { testcase("RPC ledger_entry malformed key"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault] = 42; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC ledger_entry malformed owner"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault][jss::owner] = 42; jvParams[jss::vault][jss::seq] = sequence; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "malformedOwner"); } { testcase("RPC ledger_entry malformed seq"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault][jss::owner] = issuer.human(); jvParams[jss::vault][jss::seq] = "foo"; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC ledger_entry negative seq"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault][jss::owner] = issuer.human(); jvParams[jss::vault][jss::seq] = -1; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC ledger_entry oversized seq"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault][jss::owner] = issuer.human(); jvParams[jss::vault][jss::seq] = 1e20; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC ledger_entry bool seq"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault][jss::owner] = issuer.human(); jvParams[jss::vault][jss::seq] = true; auto jvVault = env.rpc("json", "ledger_entry", to_string(jvParams)); BEAST_EXPECT( jvVault[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC account_objects"); Json::Value jvParams; jvParams[jss::account] = owner.human(); jvParams[jss::type] = jss::vault; auto jv = env.rpc( "json", "account_objects", to_string(jvParams))[jss::result]; BEAST_EXPECT(jv[jss::account_objects].size() == 1); check(jv[jss::account_objects][0u]); } { testcase("RPC ledger_data"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::binary] = false; jvParams[jss::type] = jss::vault; Json::Value jv = env.rpc("json", "ledger_data", to_string(jvParams)); BEAST_EXPECT(jv[jss::result][jss::state].size() == 1); check(jv[jss::result][jss::state][0u]); } { testcase("RPC vault_info command line"); Json::Value jv = env.rpc("vault_info", strHex(keylet.key), "validated"); BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); BEAST_EXPECT(jv[jss::result].isMember(jss::vault)); check( jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]); } { testcase("RPC vault_info json"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault_id] = strHex(keylet.key); auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); BEAST_EXPECT(jv[jss::result].isMember(jss::vault)); check( jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]); } { testcase("RPC vault_info invalid vault_id"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault_id] = "foobar"; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json invalid index"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault_id] = 0; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json by owner and sequence"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = owner.human(); jvParams[jss::seq] = sequence; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT(!jv[jss::result].isMember(jss::error)); BEAST_EXPECT(jv[jss::result].isMember(jss::vault)); check( jv[jss::result][jss::vault], jv[jss::result][jss::vault][jss::shares]); } { testcase("RPC vault_info json malformed sequence"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = owner.human(); jvParams[jss::seq] = "foobar"; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json invalid sequence"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = owner.human(); jvParams[jss::seq] = 0; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json negative sequence"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = owner.human(); jvParams[jss::seq] = -1; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json oversized sequence"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = owner.human(); jvParams[jss::seq] = 1e20; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json bool sequence"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = owner.human(); jvParams[jss::seq] = true; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json malformed owner"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = "foobar"; jvParams[jss::seq] = sequence; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json invalid combination only owner"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::owner] = owner.human(); auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json invalid combination only seq"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::seq] = sequence; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json invalid combination seq vault_id"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault_id] = strHex(keylet.key); jvParams[jss::seq] = sequence; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json invalid combination owner vault_id"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault_id] = strHex(keylet.key); jvParams[jss::owner] = owner.human(); auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase( "RPC vault_info json invalid combination owner seq " "vault_id"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; jvParams[jss::vault_id] = strHex(keylet.key); jvParams[jss::seq] = sequence; jvParams[jss::owner] = owner.human(); auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info json no input"); Json::Value jvParams; jvParams[jss::ledger_index] = jss::validated; auto jv = env.rpc("json", "vault_info", to_string(jvParams)); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info command line invalid index"); Json::Value jv = env.rpc("vault_info", "foobar", "validated"); BEAST_EXPECT(jv[jss::error].asString() == "invalidParams"); } { testcase("RPC vault_info command line invalid index"); Json::Value jv = env.rpc("vault_info", "0", "validated"); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "malformedRequest"); } { testcase("RPC vault_info command line invalid index"); Json::Value jv = env.rpc("vault_info", strHex(uint256(42)), "validated"); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "entryNotFound"); } { testcase("RPC vault_info command line invalid ledger"); Json::Value jv = env.rpc("vault_info", strHex(keylet.key), "0"); BEAST_EXPECT( jv[jss::result][jss::error].asString() == "lgrNotFound"); } } void testDelegate() { using namespace test::jtx; Env env(*this, testable_amendments()); Account alice{"alice"}; Account bob{"bob"}; Account carol{"carol"}; struct CaseArgs { PrettyAsset asset = xrpIssue(); }; auto const xrpBalance = [this]( Env const& env, Account const& account) -> std::optional { auto sle = env.le(keylet::account(account.id())); if (BEAST_EXPECT(sle != nullptr)) return sle->getFieldAmount(sfBalance).xrp().drops(); return std::nullopt; }; auto testCase = [&, this](auto test, CaseArgs args = {}) { Env env{*this, testable_amendments() | featureSingleAssetVault}; Vault vault{env}; // use different initial amount to distinguish the source balance env.fund(XRP(10000), alice); env.fund(XRP(20000), bob); env.fund(XRP(30000), carol); env.close(); env(delegate::set( carol, alice, {"Payment", "VaultCreate", "VaultSet", "VaultDelete", "VaultDeposit", "VaultWithdraw", "VaultClawback"})); test(env, vault, args.asset); }; testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) { testcase("delegated vault creation"); auto startBalance = xrpBalance(env, carol); if (!BEAST_EXPECT(startBalance.has_value())) return; auto [tx, keylet] = vault.create({.owner = carol, .asset = asset}); env(tx, delegate::as(alice)); env.close(); BEAST_EXPECT(xrpBalance(env, carol) == *startBalance); }); testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) { testcase("delegated deposit and withdrawal"); auto [tx, keylet] = vault.create({.owner = carol, .asset = asset}); env(tx); env.close(); auto const amount = 1513; auto const baseFee = env.current()->fees().base; auto startBalance = xrpBalance(env, carol); if (!BEAST_EXPECT(startBalance.has_value())) return; tx = vault.deposit( {.depositor = carol, .id = keylet.key, .amount = asset(amount)}); env(tx, delegate::as(alice)); env.close(); BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount); tx = vault.withdraw( {.depositor = carol, .id = keylet.key, .amount = asset(amount - 1)}); env(tx, delegate::as(alice)); env.close(); BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - 1); tx = vault.withdraw( {.depositor = carol, .id = keylet.key, .amount = asset(1)}); env(tx); env.close(); BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee); }); testCase([&, this](Env& env, Vault& vault, PrettyAsset const& asset) { testcase("delegated withdrawal same as base fee and deletion"); auto [tx, keylet] = vault.create({.owner = carol, .asset = asset}); env(tx); env.close(); auto const amount = 25537; auto const baseFee = env.current()->fees().base; auto startBalance = xrpBalance(env, carol); if (!BEAST_EXPECT(startBalance.has_value())) return; tx = vault.deposit( {.depositor = carol, .id = keylet.key, .amount = asset(amount)}); env(tx); env.close(); BEAST_EXPECT( xrpBalance(env, carol) == *startBalance - amount - baseFee); tx = vault.withdraw( {.depositor = carol, .id = keylet.key, .amount = asset(baseFee)}); env(tx, delegate::as(alice)); env.close(); BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - amount); tx = vault.withdraw( {.depositor = carol, .id = keylet.key, .amount = asset(amount - baseFee)}); env(tx, delegate::as(alice)); env.close(); BEAST_EXPECT(xrpBalance(env, carol) == *startBalance - baseFee); tx = vault.del({.owner = carol, .id = keylet.key}); env(tx, delegate::as(alice)); env.close(); }); } public: void run() override { testSequences(); testPreflight(); testCreateFailXRP(); testCreateFailIOU(); testCreateFailMPT(); testWithMPT(); testWithIOU(); testWithDomainCheck(); testWithDomainCheckXRP(); testNonTransferableShares(); testFailedPseudoAccount(); testScaleIOU(); testRPC(); testDelegate(); } }; BEAST_DEFINE_TESTSUITE_PRIO(Vault, app, ripple, 1); } // namespace ripple