diff --git a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp index 07fe9957a..c335f8d28 100644 --- a/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenAcceptOffer.cpp @@ -168,10 +168,21 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) dest.has_value() && *dest != ctx.tx[sfAccount]) return tecNO_PERMISSION; } - // The account offering to buy must have funds: - auto const needed = bo->at(sfAmount); - if (accountHolds( + // The account offering to buy must have funds: + // + // After this amendment, we allow an IOU issuer to buy an NFT with their + // own currency + auto const needed = bo->at(sfAmount); + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountFunds( + ctx.view, (*bo)[sfOwner], needed, fhZERO_IF_FROZEN, ctx.j) < + needed) + return tecINSUFFICIENT_FUNDS; + } + else if ( + accountHolds( ctx.view, (*bo)[sfOwner], needed.getCurrency(), @@ -206,15 +217,39 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx) // The account offering to buy must have funds: auto const needed = so->at(sfAmount); - - if (accountHolds( - ctx.view, - ctx.tx[sfAccount], - needed.getCurrency(), - needed.getIssuer(), - fhZERO_IF_FROZEN, - ctx.j) < needed) - return tecINSUFFICIENT_FUNDS; + if (!ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountHolds( + ctx.view, + ctx.tx[sfAccount], + needed.getCurrency(), + needed.getIssuer(), + fhZERO_IF_FROZEN, + ctx.j) < needed) + return tecINSUFFICIENT_FUNDS; + } + else if (!bo) + { + // After this amendment, we allow buyers to buy with their own + // issued currency. + // + // In the case of brokered mode, this check is essentially + // redundant, since we have already confirmed that buy offer is > + // than the sell offer, and that the buyer can cover the buy + // offer. + // + // We also _must not_ check the tx submitter in brokered + // mode, because then we are confirming that the broker can + // cover what the buyer will pay, which doesn't make sense, causes + // an unncessary tec, and is also resolved with this amendment. + if (accountFunds( + ctx.view, + ctx.tx[sfAccount], + needed, + fhZERO_IF_FROZEN, + ctx.j) < needed) + return tecINSUFFICIENT_FUNDS; + } } return tesSUCCESS; @@ -230,7 +265,22 @@ NFTokenAcceptOffer::pay( if (amount < beast::zero) return tecINTERNAL; - return accountSend(view(), from, to, amount, j_); + auto const result = accountSend(view(), from, to, amount, j_); + + // After this amendment, if any payment would cause a non-IOU-issuer to + // have a negative balance, or an IOU-issuer to have a positive balance in + // their own currency, we know that something went wrong. This was + // originally found in the context of IOU transfer fees. Since there are + // several payouts in this tx, just confirm that the end state is OK. + if (!view().rules().enabled(fixUnburnableNFToken)) + return result; + if (result != tesSUCCESS) + return result; + if (accountFunds(view(), from, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + return tecINSUFFICIENT_FUNDS; + if (accountFunds(view(), to, amount, fhZERO_IF_FROZEN, j_).signum() < 0) + return tecINSUFFICIENT_FUNDS; + return tesSUCCESS; } TER diff --git a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp index 695efdd0a..ff8668e44 100644 --- a/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp +++ b/src/ripple/app/tx/impl/NFTokenCreateOffer.cpp @@ -153,15 +153,28 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx) // offer may later become unfunded. if (!isSellOffer) { - auto const funds = accountHolds( - ctx.view, - ctx.tx[sfAccount], - amount.getCurrency(), - amount.getIssuer(), - FreezeHandling::fhZERO_IF_FROZEN, - ctx.j); - - if (funds.signum() <= 0) + // After this amendment, we allow an IOU issuer to make a buy offer + // using their own currency. + if (ctx.view.rules().enabled(fixUnburnableNFToken)) + { + if (accountFunds( + ctx.view, + ctx.tx[sfAccount], + amount, + FreezeHandling::fhZERO_IF_FROZEN, + ctx.j) + .signum() <= 0) + return tecUNFUNDED_OFFER; + } + else if ( + accountHolds( + ctx.view, + ctx.tx[sfAccount], + amount.getCurrency(), + amount.getIssuer(), + FreezeHandling::fhZERO_IF_FROZEN, + ctx.j) + .signum() <= 0) return tecUNFUNDED_OFFER; } diff --git a/src/ripple/ledger/View.h b/src/ripple/ledger/View.h index ee9171155..24a647c76 100644 --- a/src/ripple/ledger/View.h +++ b/src/ripple/ledger/View.h @@ -97,6 +97,11 @@ accountHolds( FreezeHandling zeroIfFrozen, beast::Journal j); +// Returns the amount an account can spend of the currency type saDefault, or +// returns saDefault if this account is the issuer of the the currency in +// question. Should be used in favor of accountHolds when questioning how much +// an account can spend while also allowing currency issuers to spend +// unlimited amounts of their own currency (since they can always issue more). [[nodiscard]] STAmount accountFunds( ReadView const& view, diff --git a/src/test/app/NFToken_test.cpp b/src/test/app/NFToken_test.cpp index 842f3f76c..33d725e5a 100644 --- a/src/test/app/NFToken_test.cpp +++ b/src/test/app/NFToken_test.cpp @@ -3827,497 +3827,586 @@ class NFToken_test : public beast::unit_test::suite using namespace test::jtx; - Env env{*this, features}; + for (auto const& tweakedFeatures : + {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) + { + Env env{*this, tweakedFeatures}; - // The most important thing to explore here is the way funds are - // assigned from the buyer to... - // o the Seller, - // o the Broker, and - // o the Issuer (in the case of a transfer fee). + // The most important thing to explore here is the way funds are + // assigned from the buyer to... + // o the Seller, + // o the Broker, and + // o the Issuer (in the case of a transfer fee). - Account const issuer{"issuer"}; - Account const minter{"minter"}; - Account const buyer{"buyer"}; - Account const broker{"broker"}; - Account const gw{"gw"}; - IOU const gwXAU(gw["XAU"]); + Account const issuer{"issuer"}; + Account const minter{"minter"}; + Account const buyer{"buyer"}; + Account const broker{"broker"}; + Account const gw{"gw"}; + IOU const gwXAU(gw["XAU"]); - env.fund(XRP(1000), issuer, minter, buyer, broker, gw); - env.close(); + env.fund(XRP(1000), issuer, minter, buyer, broker, gw); + env.close(); - env(trust(issuer, gwXAU(2000))); - env(trust(minter, gwXAU(2000))); - env(trust(buyer, gwXAU(2000))); - env(trust(broker, gwXAU(2000))); - env.close(); + env(trust(issuer, gwXAU(2000))); + env(trust(minter, gwXAU(2000))); + env(trust(buyer, gwXAU(2000))); + env(trust(broker, gwXAU(2000))); + env.close(); - env(token::setMinter(issuer, minter)); - env.close(); + env(token::setMinter(issuer, minter)); + env.close(); - // Lambda to check owner count of all accounts is one. - auto checkOwnerCountIsOne = - [this, &env]( - std::initializer_list> - accounts, - int line) { - for (Account const& acct : accounts) - { - if (std::uint32_t ownerCount = this->ownerCount(env, acct); - ownerCount != 1) + // Lambda to check owner count of all accounts is one. + auto checkOwnerCountIsOne = + [this, &env]( + std::initializer_list> + accounts, + int line) { + for (Account const& acct : accounts) { - std::stringstream ss; - ss << "Account " << acct.human() - << " expected ownerCount == 1. Got " << ownerCount; - fail(ss.str(), __FILE__, line); + if (std::uint32_t ownerCount = + this->ownerCount(env, acct); + ownerCount != 1) + { + std::stringstream ss; + ss << "Account " << acct.human() + << " expected ownerCount == 1. Got " + << ownerCount; + fail(ss.str(), __FILE__, line); + } } - } + }; + + // Lambda that mints an NFT and returns the nftID. + auto mintNFT = [&env, &issuer, &minter](std::uint16_t xferFee = 0) { + uint256 const nftID = + token::getNextID(env, issuer, 0, tfTransferable, xferFee); + env(token::mint(minter, 0), + token::issuer(issuer), + token::xferFee(xferFee), + txflags(tfTransferable)); + env.close(); + return nftID; }; - // Lambda that mints an NFT and returns the nftID. - auto mintNFT = [&env, &issuer, &minter](std::uint16_t xferFee = 0) { - uint256 const nftID = - token::getNextID(env, issuer, 0, tfTransferable, xferFee); - env(token::mint(minter, 0), - token::issuer(issuer), - token::xferFee(xferFee), - txflags(tfTransferable)); - env.close(); - return nftID; - }; - - // o Seller is selling for zero XRP. - // o Broker charges no fee. - // o No transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(); - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); - - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); - - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); - - // Broker charges no brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); - env.close(); - - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(1)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - - // o Seller is selling for zero XRP. - // o Broker charges a fee. - // o No transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(); - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); - - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); - - // Broker attempts to charge a 1.1 XRP brokerFee and fails. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(1.1)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); - - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); - - // Broker charges a 0.5 XRP brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(0.5))); - env.close(); - - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT( - env.balance(broker) == brokerBalance + XRP(0.5) - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - - // o Seller is selling for zero XRP. - // o Broker charges no fee. - // o 50% transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(maxTransferFee); - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); - - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); - - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); - - // Broker charges no brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); - env.close(); - - // Note that minter's XRP balance goes up even though they - // requested XRP(0). - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.5)); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - - // o Seller is selling for zero XRP. - // o Broker charges 0.5 XRP. - // o 50% transfer fee. - // - // Since minter is selling for zero the currency must be XRP. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(maxTransferFee); - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, XRP(0)), - txflags(tfSellNFToken)); - env.close(); - - // buyer creates their offer. Note: a buy offer can never - // offer zero. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); - env.close(); - - auto const minterBalance = env.balance(minter); - auto const buyerBalance = env.balance(buyer); - auto const brokerBalance = env.balance(broker); - auto const issuerBalance = env.balance(issuer); - - // Broker charges a 0.75 XRP brokerFee. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(XRP(0.75))); - env.close(); - - // Note that, with a 50% transfer fee, issuer gets 1/2 of what's - // left _after_ broker takes their fee. minter gets the remainder - // after both broker and minter take their cuts - BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.125)); - BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); - BEAST_EXPECT( - env.balance(broker) == brokerBalance + XRP(0.75) - drops(10)); - BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.125)); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - - // Lambda to set the balance of all passed in accounts to gwXAU(1000). - auto setXAUBalance_1000 = - [this, &gw, &gwXAU, &env]( - std::initializer_list> - accounts, - int line) { - for (Account const& acct : accounts) - { - static const auto xau1000 = gwXAU(1000); - auto const balance = env.balance(acct, gwXAU); - if (balance < xau1000) - { - env(pay(gw, acct, xau1000 - balance)); - env.close(); - } - else if (balance > xau1000) - { - env(pay(acct, gw, balance - xau1000)); - env.close(); - } - if (env.balance(acct, gwXAU) != xau1000) - { - std::stringstream ss; - ss << "Unable to set " << acct.human() - << " account balance to gwXAU(1000)"; - this->fail(ss.str(), __FILE__, line); - } - } - }; - - // The buyer and seller have identical amounts and there is no - // transfer fee. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(); - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(1000)), - txflags(tfSellNFToken)); - env.close(); - + // o Seller is selling for zero XRP. + // o Broker charges no fee. + // o No transfer fee. + // + // Since minter is selling for zero the currency must be XRP. { - // buyer creates an offer for more XAU than they currently own. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + + uint256 const nftID = mintNFT(); + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates their offer. Note: a buy offer can never + // offer zero. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1001)), + env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); + + // Broker charges no brokerFee. env(token::brokerOffers( - broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_FUNDS)); + broker, buyOfferIndex, minterOfferIndex)); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(1)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } + + // o Seller is selling for zero XRP. + // o Broker charges a fee. + // o No transfer fee. + // + // Since minter is selling for zero the currency must be XRP. { - // buyer creates an offer for less that what minter is asking. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + + uint256 const nftID = mintNFT(); + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates their offer. Note: a buy offer can never + // offer zero. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(999)), + env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + // Broker attempts to charge a 1.1 XRP brokerFee and fails. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(XRP(1.1)), ter(tecINSUFFICIENT_PAYMENT)); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); + + // Broker charges a 0.5 XRP brokerFee. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(XRP(0.5))); + env.close(); + + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokerBalance + XRP(0.5) - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); - - // Broker attempts to charge a brokerFee but cannot. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(0.1)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); - - // broker charges no brokerFee and succeeds. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex)); - env.close(); - - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(2000)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1000)); - - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - - // seller offers more than buyer is asking. - // There are both transfer and broker fees. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); - - uint256 const nftID = mintNFT(maxTransferFee); - - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(900)), - txflags(tfSellNFToken)); - env.close(); + // o Seller is selling for zero XRP. + // o Broker charges no fee. + // o 50% transfer fee. + // + // Since minter is selling for zero the currency must be XRP. { - // buyer creates an offer for more XAU than they currently own. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + + uint256 const nftID = mintNFT(maxTransferFee); + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates their offer. Note: a buy offer can never + // offer zero. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1001)), + env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); + + // Broker charges no brokerFee. env(token::brokerOffers( - broker, buyOfferIndex, minterOfferIndex), - ter(tecINSUFFICIENT_FUNDS)); + broker, buyOfferIndex, minterOfferIndex)); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + // Note that minter's XRP balance goes up even though they + // requested XRP(0). + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.5)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT(env.balance(broker) == brokerBalance - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.5)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } + + // o Seller is selling for zero XRP. + // o Broker charges 0.5 XRP. + // o 50% transfer fee. + // + // Since minter is selling for zero the currency must be XRP. { - // buyer creates an offer for less that what minter is asking. + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + + uint256 const nftID = mintNFT(maxTransferFee); + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, XRP(0)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates their offer. Note: a buy offer can never + // offer zero. uint256 const buyOfferIndex = keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(899)), + env(token::createOffer(buyer, nftID, XRP(1)), token::owner(minter)); env.close(); - // broker attempts to broker the offers but cannot. + auto const minterBalance = env.balance(minter); + auto const buyerBalance = env.balance(buyer); + auto const brokerBalance = env.balance(broker); + auto const issuerBalance = env.balance(issuer); + + // Broker charges a 0.75 XRP brokerFee. env(token::brokerOffers( broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(XRP(0.75))); + env.close(); + + // Note that, with a 50% transfer fee, issuer gets 1/2 of what's + // left _after_ broker takes their fee. minter gets the + // remainder after both broker and minter take their cuts + BEAST_EXPECT(env.balance(minter) == minterBalance + XRP(0.125)); + BEAST_EXPECT(env.balance(buyer) == buyerBalance - XRP(1)); + BEAST_EXPECT( + env.balance(broker) == + brokerBalance + XRP(0.75) - drops(10)); + BEAST_EXPECT(env.balance(issuer) == issuerBalance + XRP(0.125)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } + + // Lambda to set the balance of all passed in accounts to + // gwXAU(amount). + auto setXAUBalance = + [this, &gw, &gwXAU, &env]( + std::initializer_list> + accounts, + int amount, + int line) { + for (Account const& acct : accounts) + { + auto const xauAmt = gwXAU(amount); + auto const balance = env.balance(acct, gwXAU); + if (balance < xauAmt) + { + env(pay(gw, acct, xauAmt - balance)); + env.close(); + } + else if (balance > xauAmt) + { + env(pay(acct, gw, balance - xauAmt)); + env.close(); + } + if (env.balance(acct, gwXAU) != xauAmt) + { + std::stringstream ss; + ss << "Unable to set " << acct.human() + << " account balance to gwXAU(" << amount << ")"; + this->fail(ss.str(), __FILE__, line); + } + } + }; + + // The buyer and seller have identical amounts and there is no + // transfer fee. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); + + uint256 const nftID = mintNFT(); + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(1000)), + txflags(tfSellNFToken)); + env.close(); + + { + // buyer creates an offer for more XAU than they currently + // own. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1001)), + token::owner(minter)); + env.close(); + + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + { + // buyer creates an offer for less that what minter is + // asking. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(999)), + token::owner(minter)); + env.close(); + + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); + + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + + // buyer creates a large enough offer. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1000)), + token::owner(minter)); + env.close(); + + // Broker attempts to charge a brokerFee but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(0.1)), ter(tecINSUFFICIENT_PAYMENT)); env.close(); - // Cancel buyer's bad offer so the next test starts in a - // clean state. - env(token::cancelOffer(buyer, {buyOfferIndex})); + // broker charges no brokerFee and succeeds. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex)); + env.close(); + + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(2000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1000)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); env.close(); } - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); - // Broker attempts to charge a brokerFee larger than the - // difference between the two offers but cannot. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(101)), - ter(tecINSUFFICIENT_PAYMENT)); - env.close(); + // seller offers more than buyer is asking. + // There are both transfer and broker fees. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); - // broker charges the full difference between the two offers and - // succeeds. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(100))); - env.close(); + uint256 const nftID = mintNFT(maxTransferFee); - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1450)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1450)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1100)); + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); + { + // buyer creates an offer for more XAU than they currently + // own. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1001)), + token::owner(minter)); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); - } - // seller offers more than buyer is asking. - // There are both transfer and broker fees, but broker takes less than - // the maximum. - { - checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); - setXAUBalance_1000({issuer, minter, buyer, broker}, __LINE__); + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); - uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + { + // buyer creates an offer for less that what minter is + // asking. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(899)), + token::owner(minter)); + env.close(); - // minter creates their offer. - uint256 const minterOfferIndex = - keylet::nftoffer(minter, env.seq(minter)).key; - env(token::createOffer(minter, nftID, gwXAU(900)), - txflags(tfSellNFToken)); - env.close(); + // broker attempts to broker the offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); - // buyer creates a large enough offer. - uint256 const buyOfferIndex = - keylet::nftoffer(buyer, env.seq(buyer)).key; - env(token::createOffer(buyer, nftID, gwXAU(1000)), - token::owner(minter)); - env.close(); + // Cancel buyer's bad offer so the next test starts in a + // clean state. + env(token::cancelOffer(buyer, {buyOfferIndex})); + env.close(); + } + // buyer creates a large enough offer. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1000)), + token::owner(minter)); + env.close(); - // broker charges half difference between the two offers and - // succeeds. 25% of the remaining difference goes to issuer. - // The rest goes to minter. - env(token::brokerOffers(broker, buyOfferIndex, minterOfferIndex), - token::brokerFee(gwXAU(50))); - env.close(); + // Broker attempts to charge a brokerFee larger than the + // difference between the two offers but cannot. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(101)), + ter(tecINSUFFICIENT_PAYMENT)); + env.close(); - BEAST_EXPECT(ownerCount(env, issuer) == 1); - BEAST_EXPECT(ownerCount(env, minter) == 1); - BEAST_EXPECT(ownerCount(env, buyer) == 2); - BEAST_EXPECT(ownerCount(env, broker) == 1); - BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); - BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); - BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); - BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1050)); + // broker charges the full difference between the two offers and + // succeeds. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(100))); + env.close(); - // Burn the NFT so the next test starts with a clean state. - env(token::burn(buyer, nftID)); - env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1450)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1450)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1100)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } + // seller offers more than buyer is asking. + // There are both transfer and broker fees, but broker takes less + // than the maximum. + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer, broker}, 1000, __LINE__); + + uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates a large enough offer. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1000)), + token::owner(minter)); + env.close(); + + // broker charges half difference between the two offers and + // succeeds. 25% of the remaining difference goes to issuer. + // The rest goes to minter. + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(50))); + env.close(); + + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(1050)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } + // Broker has a balance less than the seller offer + { + checkOwnerCountIsOne({issuer, minter, buyer, broker}, __LINE__); + setXAUBalance({issuer, minter, buyer}, 1000, __LINE__); + setXAUBalance({broker}, 500, __LINE__); + uint256 const nftID = mintNFT(maxTransferFee / 2); // 25% + + // minter creates their offer. + uint256 const minterOfferIndex = + keylet::nftoffer(minter, env.seq(minter)).key; + env(token::createOffer(minter, nftID, gwXAU(900)), + txflags(tfSellNFToken)); + env.close(); + + // buyer creates a large enough offer. + uint256 const buyOfferIndex = + keylet::nftoffer(buyer, env.seq(buyer)).key; + env(token::createOffer(buyer, nftID, gwXAU(1000)), + token::owner(minter)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(50))); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 1); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1237.5)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1712.5)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(550)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(buyer, nftID)); + env.close(); + } + else + { + env(token::brokerOffers( + broker, buyOfferIndex, minterOfferIndex), + token::brokerFee(gwXAU(50)), + ter(tecINSUFFICIENT_FUNDS)); + env.close(); + BEAST_EXPECT(ownerCount(env, issuer) == 1); + BEAST_EXPECT(ownerCount(env, minter) == 3); + BEAST_EXPECT(ownerCount(env, buyer) == 2); + BEAST_EXPECT(ownerCount(env, broker) == 1); + BEAST_EXPECT(env.balance(issuer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(500)); + + // Burn the NFT so the next test starts with a clean state. + env(token::burn(minter, nftID)); + env.close(); + } + } } } @@ -4823,9 +4912,14 @@ class NFToken_test : public beast::unit_test::suite Account const gw{"gw"}; IOU const gwXAU(gw["XAU"]); - // Test both with and without fixNFTokenNegOffer + // Test both with and without fixNFTokenNegOffer and + // fixUnburnableNFToken. Need to turn off fixUnburnableNFToken as well + // because that amendment came later and addressed the acceptance + // side of this issue. for (auto const& tweakedFeatures : - {features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1, + {features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1 - + fixUnburnableNFToken, + features - fixNFTokenNegOffer - featureNonFungibleTokensV1_1, features | fixNFTokenNegOffer}) { // There was a bug in the initial NFT implementation that @@ -4914,8 +5008,10 @@ class NFToken_test : public beast::unit_test::suite env.close(); } { - // 1. If fixNFTokenNegOffer is NOT enabled get tecSUCCESS. - // 2. If fixNFTokenNegOffer IS enabled get tecOBJECT_NOT_FOUND. + // 1. If fixNFTokenNegOffer is enabled get tecOBJECT_NOT_FOUND + // 2. If it is not enabled, but fixUnburnableNFToken is + // enabled, get tecOBJECT_NOT_FOUND. + // 3. If neither are enabled, get tesSUCCESS. TER const offerAcceptTER = tweakedFeatures[fixNFTokenNegOffer] ? static_cast(tecOBJECT_NOT_FOUND) : static_cast(tesSUCCESS); @@ -5047,6 +5143,757 @@ class NFToken_test : public beast::unit_test::suite } } + void + testIOUWithTransferFee(FeatureBitset features) + { + using namespace test::jtx; + + testcase("Payments with IOU transfer fees"); + + for (auto const& tweakedFeatures : + {features - fixUnburnableNFToken, features | fixUnburnableNFToken}) + { + Env env{*this, tweakedFeatures}; + + Account const minter{"minter"}; + Account const secondarySeller{"seller"}; + Account const buyer{"buyer"}; + Account const gw{"gateway"}; + Account const broker{"broker"}; + IOU const gwXAU(gw["XAU"]); + IOU const gwXPB(gw["XPB"]); + + env.fund(XRP(1000), gw, minter, secondarySeller, buyer, broker); + env.close(); + + env(trust(minter, gwXAU(2000))); + env(trust(secondarySeller, gwXAU(2000))); + env(trust(broker, gwXAU(10000))); + env(trust(buyer, gwXAU(2000))); + env(trust(buyer, gwXPB(2000))); + env.close(); + + // The IOU issuer has a 2% transfer rate + env(rate(gw, 1.02)); + env.close(); + + auto expectInitialState = [this, + &env, + &buyer, + &minter, + &secondarySeller, + &broker, + &gw, + &gwXAU, + &gwXPB]() { + // Buyer should have XAU 1000, XPB 0 + // Minter should have XAU 0, XPB 0 + // Secondary seller should have XAU 0, XPB 0 + // Broker should have XAU 5000, XPB 0 + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(secondarySeller, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5000)); + BEAST_EXPECT(env.balance(broker, gwXPB) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(0)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(0)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XPB"]) == gwXPB(0)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5000)); + BEAST_EXPECT(env.balance(gw, broker["XPB"]) == gwXPB(0)); + }; + + auto reinitializeTrustLineBalances = [&expectInitialState, + &env, + &buyer, + &minter, + &secondarySeller, + &broker, + &gw, + &gwXAU, + &gwXPB]() { + if (auto const difference = + gwXAU(1000) - env.balance(buyer, gwXAU); + difference > gwXAU(0)) + env(pay(gw, buyer, difference)); + if (env.balance(buyer, gwXPB) > gwXPB(0)) + env(pay(buyer, gw, env.balance(buyer, gwXPB))); + if (env.balance(minter, gwXAU) > gwXAU(0)) + env(pay(minter, gw, env.balance(minter, gwXAU))); + if (env.balance(minter, gwXPB) > gwXPB(0)) + env(pay(minter, gw, env.balance(minter, gwXPB))); + if (env.balance(secondarySeller, gwXAU) > gwXAU(0)) + env( + pay(secondarySeller, + gw, + env.balance(secondarySeller, gwXAU))); + if (env.balance(secondarySeller, gwXPB) > gwXPB(0)) + env( + pay(secondarySeller, + gw, + env.balance(secondarySeller, gwXPB))); + auto brokerDiff = gwXAU(5000) - env.balance(broker, gwXAU); + if (brokerDiff > gwXAU(0)) + env(pay(gw, broker, brokerDiff)); + else if (brokerDiff < gwXAU(0)) + { + brokerDiff.negate(); + env(pay(broker, gw, brokerDiff)); + } + if (env.balance(broker, gwXPB) > gwXPB(0)) + env(pay(broker, gw, env.balance(broker, gwXPB))); + env.close(); + expectInitialState(); + }; + + auto mintNFT = [&env](Account const& minter, int transferFee = 0) { + uint256 const nftID = token::getNextID( + env, minter, 0, tfTransferable, transferFee); + env(token::mint(minter), + token::xferFee(transferFee), + txflags(tfTransferable)); + env.close(); + return nftID; + }; + + auto createBuyOffer = + [&env]( + Account const& offerer, + Account const& owner, + uint256 const& nftID, + STAmount const& amount, + std::optional const terCode = {}) { + uint256 const offerID = + keylet::nftoffer(offerer, env.seq(offerer)).key; + env(token::createOffer(offerer, nftID, amount), + token::owner(owner), + terCode ? ter(*terCode) + : ter(static_cast(tesSUCCESS))); + env.close(); + return offerID; + }; + + auto createSellOffer = + [&env]( + Account const& offerer, + uint256 const& nftID, + STAmount const& amount, + std::optional const terCode = {}) { + uint256 const offerID = + keylet::nftoffer(offerer, env.seq(offerer)).key; + env(token::createOffer(offerer, nftID, amount), + txflags(tfSellNFToken), + terCode ? ter(*terCode) + : ter(static_cast(tesSUCCESS))); + env.close(); + return offerID; + }; + + { + // Buyer attempts to send 100% of their balance of an IOU + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // Buyer attempts to send 100% of their balance of an IOU + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU, but such that the addition of the transfer + // fee would be greater than the buyer's balance (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXAU(995)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(995)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-14.9)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-995)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(14.9)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU, but such that the addition of the transfer + // fee would be greater than the buyer's balance (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(995)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(995)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-14.9)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-995)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(14.9)); + } + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is still less than their balance + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXAU(900)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-900)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is still less than their balance + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(900)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(900)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-900)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is equal than their balance + // (sellside) + reinitializeTrustLineBalances(); + + // pay them an additional XAU 20 to cover transfer rate + env(pay(gw, buyer, gwXAU(20))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Buyer attempts to send an amount less than 100% of their + // balance of an IOU with a transfer fee, and such that the + // addition of the transfer fee is equal than their balance + // (buyside) + reinitializeTrustLineBalances(); + + // pay them an additional XAU 20 to cover transfer rate + env(pay(gw, buyer, gwXAU(20))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXAU(1000)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway attempts to buy NFT with their own IOU - no + // transfer fee is calculated here (sellside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecINSUFFICIENT_FUNDS); + env(token::acceptSellOffer(gw, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU - no + // transfer fee is calculated here (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecUNFUNDED_OFFER); + auto const offerID = + createBuyOffer(gw, minter, nftID, gwXAU(1000), {offerTER}); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecOBJECT_NOT_FOUND); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(1000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-1000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU for more + // than minter trusts (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = + createSellOffer(minter, nftID, gwXAU(5000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecINSUFFICIENT_FUNDS); + env(token::acceptSellOffer(gw, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-5000)); + } + else + expectInitialState(); + } + { + // Gateway attempts to buy NFT with their own IOU for more + // than minter trusts (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const offerTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecUNFUNDED_OFFER); + auto const offerID = + createBuyOffer(gw, minter, nftID, gwXAU(5000), {offerTER}); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tesSUCCESS) + : static_cast(tecOBJECT_NOT_FOUND); + env(token::acceptBuyOffer(minter, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(5000)); + BEAST_EXPECT( + env.balance(gw, minter["XAU"]) == gwXAU(-5000)); + } + else + expectInitialState(); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that would be greater than a balance if there were a + // transfer fee calculated in this transaction. (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = createSellOffer(gw, nftID, gwXAU(1000)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that would be greater than a balance if there were a + // transfer fee calculated in this transaction. (buyside) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(gw); + auto const offerID = + createBuyOffer(buyer, gw, nftID, gwXAU(1000)); + env(token::acceptBuyOffer(gw, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(0)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(0)); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that is greater than a balance before transfer fees. + // (sellside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = createSellOffer(gw, nftID, gwXAU(2000)); + env(token::acceptSellOffer(buyer, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Gateway is the NFT minter and attempts to sell NFT for an + // amount that is greater than a balance before transfer fees. + // (buyside) + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(gw); + auto const offerID = + createBuyOffer(buyer, gw, nftID, gwXAU(2000)); + env(token::acceptBuyOffer(gw, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10, which they + // have no trust line for and buyer has none of (sellside). + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXPB(10)); + env(token::acceptSellOffer(buyer, offerID), + ter(static_cast(tecINSUFFICIENT_FUNDS))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10, which they + // have no trust line for and buyer has none of (buyside). + reinitializeTrustLineBalances(); + auto const nftID = mintNFT(minter); + auto const offerID = createBuyOffer( + buyer, + minter, + nftID, + gwXPB(10), + {static_cast(tecUNFUNDED_OFFER)}); + env(token::acceptBuyOffer(minter, offerID), + ter(static_cast(tecOBJECT_NOT_FOUND))); + env.close(); + expectInitialState(); + } + { + // Minter attempts to sell the token for XPB 10 and the buyer + // has it but the minter has no trust line. Trust line is + // created as a result of the tx (sellside). + reinitializeTrustLineBalances(); + env(pay(gw, buyer, gwXPB(100))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = createSellOffer(minter, nftID, gwXPB(10)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(10)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(89.8)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(-10)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(-89.8)); + } + { + // Minter attempts to sell the token for XPB 10 and the buyer + // has it but the minter has no trust line. Trust line is + // created as a result of the tx (buyside). + reinitializeTrustLineBalances(); + env(pay(gw, buyer, gwXPB(100))); + env.close(); + + auto const nftID = mintNFT(minter); + auto const offerID = + createBuyOffer(buyer, minter, nftID, gwXPB(10)); + env(token::acceptBuyOffer(minter, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXPB) == gwXPB(10)); + BEAST_EXPECT(env.balance(buyer, gwXPB) == gwXPB(89.8)); + BEAST_EXPECT(env.balance(gw, minter["XPB"]) == gwXPB(-10)); + BEAST_EXPECT(env.balance(gw, buyer["XPB"]) == gwXPB(-89.8)); + } + { + // There is a transfer fee on the NFT and buyer has exact + // amount (sellside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createSellOffer(secondarySeller, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptSellOffer(buyer, offerID), ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(30)); + BEAST_EXPECT( + env.balance(secondarySeller, gwXAU) == gwXAU(970)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-30)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-970)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // There is a transfer fee on the NFT and buyer has exact + // amount (buyside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(1000)); + auto const sellTER = tweakedFeatures[fixUnburnableNFToken] + ? static_cast(tecINSUFFICIENT_FUNDS) + : static_cast(tesSUCCESS); + env(token::acceptBuyOffer(secondarySeller, offerID), + ter(sellTER)); + env.close(); + + if (tweakedFeatures[fixUnburnableNFToken]) + expectInitialState(); + else + { + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(30)); + BEAST_EXPECT( + env.balance(secondarySeller, gwXAU) == gwXAU(970)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(-20)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-30)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-970)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(20)); + } + } + { + // There is a transfer fee on the NFT and buyer has enough + // (sellside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createSellOffer(secondarySeller, nftID, gwXAU(900)); + env(token::acceptSellOffer(buyer, offerID)); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(27)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(873)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-27)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-873)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // There is a transfer fee on the NFT and buyer has enough + // (buyside) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const offerID = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(900)); + env(token::acceptBuyOffer(secondarySeller, offerID)); + env.close(); + + // receives 3% of 900 - 27 + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(27)); + // receives 97% of 900 - 873 + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(873)); + // pays 900 plus 2% transfer fee on XAU - 918 + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(82)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-27)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-873)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-82)); + } + { + // There is a broker fee on the NFT. XAU transfer fee is only + // calculated from the buyer's output, not deducted from + // broker fee. + // + // For a payment of 500 with a 2% IOU transfee fee and 100 + // broker fee: + // + // A) Total sale amount + IOU transfer fee is paid by buyer + // (Buyer pays (1.02 * 500) = 510) + // B) GW receives the additional IOU transfer fee + // (GW receives 10 from buyer calculated above) + // C) Broker receives broker fee (no IOU transfer fee) + // (Broker receives 100 from buyer) + // D) Seller receives balance (no IOU transfer fee) + // (Seller receives (510 - 10 - 100) = 400) + reinitializeTrustLineBalances(); + + auto const nftID = mintNFT(minter); + auto const sellOffer = + createSellOffer(minter, nftID, gwXAU(300)); + auto const buyOffer = + createBuyOffer(buyer, minter, nftID, gwXAU(500)); + env(token::brokerOffers(broker, buyOffer, sellOffer), + token::brokerFee(gwXAU(100))); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(400)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(490)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5100)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-400)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-490)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5100)); + } + { + // There is broker and transfer fee on the NFT + // + // For a payment of 500 with a 2% IOU transfer fee, 3% NFT + // transfer fee, and 100 broker fee: + // + // A) Total sale amount + IOU transfer fee is paid by buyer + // (Buyer pays (1.02 * 500) = 510) + // B) GW receives the additional IOU transfer fee + // (GW receives 10 from buyer calculated above) + // C) Broker receives broker fee (no IOU transfer fee) + // (Broker receives 100 from buyer) + // D) Minter receives transfer fee (no IOU transfer fee) + // (Minter receives 0.03 * (510 - 10 - 100) = 12) + // E) Seller receives balance (no IOU transfer fee) + // (Seller receives (510 - 10 - 100 - 12) = 388) + reinitializeTrustLineBalances(); + + // secondarySeller has to sell it because transfer fees only + // happen on secondary sales + auto const nftID = mintNFT(minter, 3000); // 3% + auto const primaryOfferID = + createSellOffer(minter, nftID, XRP(0)); + env(token::acceptSellOffer(secondarySeller, primaryOfferID)); + env.close(); + + // now we can do a secondary sale + auto const sellOffer = + createSellOffer(secondarySeller, nftID, gwXAU(300)); + auto const buyOffer = + createBuyOffer(buyer, secondarySeller, nftID, gwXAU(500)); + env(token::brokerOffers(broker, buyOffer, sellOffer), + token::brokerFee(gwXAU(100))); + env.close(); + + BEAST_EXPECT(env.balance(minter, gwXAU) == gwXAU(12)); + BEAST_EXPECT(env.balance(buyer, gwXAU) == gwXAU(490)); + BEAST_EXPECT(env.balance(secondarySeller, gwXAU) == gwXAU(388)); + BEAST_EXPECT(env.balance(broker, gwXAU) == gwXAU(5100)); + BEAST_EXPECT(env.balance(gw, minter["XAU"]) == gwXAU(-12)); + BEAST_EXPECT(env.balance(gw, buyer["XAU"]) == gwXAU(-490)); + BEAST_EXPECT( + env.balance(gw, secondarySeller["XAU"]) == gwXAU(-388)); + BEAST_EXPECT(env.balance(gw, broker["XAU"]) == gwXAU(-5100)); + } + } + } + void testWithFeats(FeatureBitset features) { @@ -5076,6 +5923,7 @@ class NFToken_test : public beast::unit_test::suite testNFTokenDeleteAccount(features); testNftXxxOffers(features); testFixNFTokenNegOffer(features); + testIOUWithTransferFee(features); } public: @@ -5086,6 +5934,8 @@ public: FeatureBitset const all{supported_amendments()}; FeatureBitset const fixNFTDir{fixNFTokenDirV1}; + // TODO too many tests are being run - ths fixNFTDir check should be + // pushed into the tests that use it testWithFeats(all - fixNFTDir); testWithFeats(all - disallowIncoming); testWithFeats(all);