diff --git a/src/ripple/app/tx/impl/CFTokenAuthorize.cpp b/src/ripple/app/tx/impl/CFTokenAuthorize.cpp new file mode 100644 index 00000000000..34ba84dfb77 --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenAuthorize.cpp @@ -0,0 +1,250 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +CFTokenAuthorize::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCFTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfCFTokenAuthorizeMask) + return temINVALID_FLAG; + + auto const accountID = ctx.tx[sfAccount]; + auto const holderID = ctx.tx[~sfCFTokenHolder]; + if (holderID && accountID == holderID) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +CFTokenAuthorize::preclaim(PreclaimContext const& ctx) +{ + auto const sleCftIssuance = + ctx.view.read(keylet::cftIssuance(ctx.tx[sfCFTokenIssuanceID])); + if (!sleCftIssuance) + return tecOBJECT_NOT_FOUND; + + auto const accountID = ctx.tx[sfAccount]; + auto const txFlags = ctx.tx.getFlags(); + auto const holderID = ctx.tx[~sfCFTokenHolder]; + + if (holderID && !(ctx.view.exists(keylet::account(*holderID)))) + return tecNO_DST; + + std::uint32_t const cftIssuanceFlags = sleCftIssuance->getFieldU32(sfFlags); + + std::shared_ptr sleCft; + + // If tx is submitted by issuer, they would either try to do the following + // for allowlisting: + // 1. authorize an account + // 2. unauthorize an account + // + // Note: `accountID` is issuer's account + // `holderID` is holder's account + if (accountID == (*sleCftIssuance)[sfIssuer]) + { + // If tx is submitted by issuer, it only applies for CFT with + // lsfCFTRequireAuth set + if (!(cftIssuanceFlags & lsfCFTRequireAuth)) + return tecNO_AUTH; + + if (!holderID) + return temMALFORMED; + + if (!ctx.view.exists( + keylet::cftoken(ctx.tx[sfCFTokenIssuanceID], *holderID))) + return tecNO_ENTRY; + + return tesSUCCESS; + } + + // if non-issuer account submits this tx, then they are trying either: + // 1. Unauthorize/delete CFToken + // 2. Use/create CFToken + // + // Note: `accountID` is holder's account + // `holderID` is NOT used + if (holderID) + return temMALFORMED; + + sleCft = + ctx.view.read(keylet::cftoken(ctx.tx[sfCFTokenIssuanceID], accountID)); + + // if holder wants to delete/unauthorize a cft + if (txFlags & tfCFTUnauthorize) + { + if (!sleCft) + return tecNO_ENTRY; + + if ((*sleCft)[sfCFTAmount] != 0) + return tecHAS_OBLIGATIONS; + } + // if holder wants to use and create a cft + else + { + if (sleCft) + return tecCFTOKEN_EXISTS; + } + + return tesSUCCESS; +} + +TER +CFTokenAuthorize::doApply() +{ + auto const cftIssuanceID = ctx_.tx[sfCFTokenIssuanceID]; + auto const sleCftIssuance = view().read(keylet::cftIssuance(cftIssuanceID)); + if (!sleCftIssuance) + return tecINTERNAL; + + auto const sleAcct = view().peek(keylet::account(account_)); + if (!sleAcct) + return tecINTERNAL; + + auto const holderID = ctx_.tx[~sfCFTokenHolder]; + auto const txFlags = ctx_.tx.getFlags(); + + // If the account that submitted this tx is the issuer of the CFT + // Note: `account_` is issuer's account + // `holderID` is holder's account + if (account_ == (*sleCftIssuance)[sfIssuer]) + { + if (!holderID) + return tecINTERNAL; + + auto const sleCft = + view().peek(keylet::cftoken(cftIssuanceID, *holderID)); + if (!sleCft) + return tecINTERNAL; + + std::uint32_t const flagsIn = sleCft->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + // Issuer wants to unauthorize the holder, unset lsfCFTAuthorized on + // their CFToken + if (txFlags & tfCFTUnauthorize) + flagsOut &= ~lsfCFTAuthorized; + // Issuer wants to authorize a holder, set lsfCFTAuthorized on their + // CFToken + else + flagsOut |= lsfCFTAuthorized; + + if (flagsIn != flagsOut) + sleCft->setFieldU32(sfFlags, flagsOut); + + view().update(sleCft); + return tesSUCCESS; + } + + // If the account that submitted the tx is a holder + // Note: `account_` is holder's account + // `holderID` is NOT used + if (holderID) + return tecINTERNAL; + + // When a holder wants to unauthorize/delete a CFT, the ledger must + // - delete cftokenKey from both owner and cft directories + // - delete the CFToken + if (txFlags & tfCFTUnauthorize) + { + auto const cftokenKey = keylet::cftoken(cftIssuanceID, account_); + auto const sleCft = view().peek(cftokenKey); + if (!sleCft) + return tecINTERNAL; + + if (!view().dirRemove( + keylet::ownerDir(account_), + (*sleCft)[sfOwnerNode], + sleCft->key(), + false)) + return tecINTERNAL; + + if (!view().dirRemove( + keylet::cft_dir(cftIssuanceID), + (*sleCft)[sfCFTokenNode], + sleCft->key(), + false)) + return tecINTERNAL; + + adjustOwnerCount( + view(), sleAcct, -1, beast::Journal{beast::Journal::getNullSink()}); + + view().erase(sleCft); + return tesSUCCESS; + } + + // A potential holder wants to authorize/hold a cft, the ledger must: + // - add the new cftokenKey to both the owner and cft directries + // - create the CFToken object for the holder + std::uint32_t const uOwnerCount = sleAcct->getFieldU32(sfOwnerCount); + XRPAmount const reserveCreate( + (uOwnerCount < 2) ? XRPAmount(beast::zero) + : view().fees().accountReserve(uOwnerCount + 1)); + + if (mPriorBalance < reserveCreate) + return tecINSUFFICIENT_RESERVE; + + auto const cftokenKey = keylet::cftoken(cftIssuanceID, account_); + + auto const ownerNode = view().dirInsert( + keylet::ownerDir(account_), cftokenKey, describeOwnerDir(account_)); + + if (!ownerNode) + return tecDIR_FULL; + + auto const cftNode = view().dirInsert( + keylet::cft_dir(cftIssuanceID), + cftokenKey, + [&cftIssuanceID](std::shared_ptr const& sle) { + (*sle)[sfCFTokenIssuanceID] = cftIssuanceID; + }); + + if (!cftNode) + return tecDIR_FULL; + + auto cftoken = std::make_shared(cftokenKey); + (*cftoken)[sfAccount] = account_; + (*cftoken)[sfCFTokenIssuanceID] = cftIssuanceID; + (*cftoken)[sfFlags] = 0; + (*cftoken)[sfCFTAmount] = 0; + (*cftoken)[sfOwnerNode] = *ownerNode; + (*cftoken)[sfCFTokenNode] = *cftNode; + view().insert(cftoken); + + // Update owner count. + adjustOwnerCount(view(), sleAcct, 1, j_); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/CFTokenAuthorize.h b/src/ripple/app/tx/impl/CFTokenAuthorize.h new file mode 100644 index 00000000000..f27120a68e7 --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenAuthorize.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CFTOKENAUTHORIZE_H_INCLUDED +#define RIPPLE_TX_CFTOKENAUTHORIZE_H_INCLUDED + +#include + +namespace ripple { + +class CFTokenAuthorize : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CFTokenAuthorize(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp b/src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp new file mode 100644 index 00000000000..38349231fa2 --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenIssuanceCreate.cpp @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +CFTokenIssuanceCreate::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCFTokensV1)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfCFTokenIssuanceCreateMask) + return temINVALID_FLAG; + + if (auto const fee = ctx.tx[~sfTransferFee]) + { + if (fee > maxTransferFee) + return temBAD_CFTOKEN_TRANSFER_FEE; + + // If a non-zero TransferFee is set then the tfTransferable flag + // must also be set. + if (fee > 0u && !ctx.tx.isFlag(tfCFTCanTransfer)) + return temMALFORMED; + } + + if (auto const metadata = ctx.tx[~sfCFTokenMetadata]) + { + if (metadata->length() == 0 || + metadata->length() > maxCFTokenMetadataLength) + return temMALFORMED; + } + + return preflight2(ctx); +} + +TER +CFTokenIssuanceCreate::preclaim(PreclaimContext const& ctx) +{ + return tesSUCCESS; +} + +TER +CFTokenIssuanceCreate::doApply() +{ + auto const acct = view().peek(keylet::account(account_)); + if (!acct) + return tecINTERNAL; + + if (mPriorBalance < view().fees().accountReserve((*acct)[sfOwnerCount] + 1)) + return tecINSUFFICIENT_RESERVE; + + auto const cftIssuanceID = + keylet::cftIssuance(account_, ctx_.tx.getSeqProxy().value()); + + // create the CFTokenIssuance + { + auto const ownerNode = view().dirInsert( + keylet::ownerDir(account_), + cftIssuanceID, + describeOwnerDir(account_)); + + if (!ownerNode) + return tecDIR_FULL; + + auto cftIssuance = std::make_shared(cftIssuanceID); + (*cftIssuance)[sfFlags] = ctx_.tx.getFlags() & ~tfUniversal; + (*cftIssuance)[sfIssuer] = account_; + (*cftIssuance)[sfOutstandingAmount] = 0; + (*cftIssuance)[sfOwnerNode] = *ownerNode; + + if (auto const max = ctx_.tx[~sfMaximumAmount]) + (*cftIssuance)[sfMaximumAmount] = *max; + + if (auto const scale = ctx_.tx[~sfAssetScale]) + (*cftIssuance)[sfAssetScale] = *scale; + + if (auto const fee = ctx_.tx[~sfTransferFee]) + (*cftIssuance)[sfTransferFee] = *fee; + + if (auto const metadata = ctx_.tx[~sfCFTokenMetadata]) + (*cftIssuance)[sfCFTokenMetadata] = *metadata; + + view().insert(cftIssuance); + } + + // Update owner count. + adjustOwnerCount(view(), acct, 1, j_); + + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/ripple/app/tx/impl/CFTokenIssuanceCreate.h b/src/ripple/app/tx/impl/CFTokenIssuanceCreate.h new file mode 100644 index 00000000000..c6f6c6f8240 --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenIssuanceCreate.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CFTOKENISSUANCECREATE_H_INCLUDED +#define RIPPLE_TX_CFTOKENISSUANCECREATE_H_INCLUDED + +#include + +namespace ripple { + +class CFTokenIssuanceCreate : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CFTokenIssuanceCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/app/tx/impl/CFTokenIssuanceDestroy.cpp b/src/ripple/app/tx/impl/CFTokenIssuanceDestroy.cpp new file mode 100644 index 00000000000..172a78d49e7 --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenIssuanceDestroy.cpp @@ -0,0 +1,86 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +CFTokenIssuanceDestroy::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCFTokensV1)) + return temDISABLED; + + // check flags + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + if (ctx.tx.getFlags() & tfCFTokenIssuanceDestroyMask) + return temINVALID_FLAG; + + return preflight2(ctx); +} + +TER +CFTokenIssuanceDestroy::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleCFT = + ctx.view.read(keylet::cftIssuance(ctx.tx[sfCFTokenIssuanceID])); + if (!sleCFT) + return tecOBJECT_NOT_FOUND; + + // ensure it is issued by the tx submitter + if ((*sleCFT)[sfIssuer] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + // ensure it has no outstanding balances + if ((*sleCFT)[~sfOutstandingAmount] != 0) + return tecHAS_OBLIGATIONS; + + return tesSUCCESS; +} + +TER +CFTokenIssuanceDestroy::doApply() +{ + auto const cft = + view().peek(keylet::cftIssuance(ctx_.tx[sfCFTokenIssuanceID])); + auto const issuer = (*cft)[sfIssuer]; + + if (!view().dirRemove( + keylet::ownerDir(issuer), (*cft)[sfOwnerNode], cft->key(), false)) + return tefBAD_LEDGER; + + view().erase(cft); + + adjustOwnerCount( + view(), + view().peek(keylet::account(issuer)), + -1, + beast::Journal{beast::Journal::getNullSink()}); + + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/ripple/app/tx/impl/CFTokenIssuanceDestroy.h b/src/ripple/app/tx/impl/CFTokenIssuanceDestroy.h new file mode 100644 index 00000000000..1cbe300f511 --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenIssuanceDestroy.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CFTOKENISSUANCEDESTROY_H_INCLUDED +#define RIPPLE_TX_CFTOKENISSUANCEDESTROY_H_INCLUDED + +#include + +namespace ripple { + +class CFTokenIssuanceDestroy : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CFTokenIssuanceDestroy(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif \ No newline at end of file diff --git a/src/ripple/app/tx/impl/CFTokenIssuanceSet.cpp b/src/ripple/app/tx/impl/CFTokenIssuanceSet.cpp new file mode 100644 index 00000000000..03c4023131f --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenIssuanceSet.cpp @@ -0,0 +1,118 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include +#include + +namespace ripple { + +NotTEC +CFTokenIssuanceSet::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureCFTokensV1)) + return temDISABLED; + + // check flags + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const txFlags = ctx.tx.getFlags(); + + if (txFlags & tfCFTokenIssuanceSetMask) + return temINVALID_FLAG; + // fails if both flags are set + else if ((txFlags & tfCFTLock) && (txFlags & tfCFTUnlock)) + return temINVALID_FLAG; + + auto const accountID = ctx.tx[sfAccount]; + auto const holderID = ctx.tx[~sfCFTokenHolder]; + if (holderID && accountID == holderID) + return temMALFORMED; + + return preflight2(ctx); +} + +TER +CFTokenIssuanceSet::preclaim(PreclaimContext const& ctx) +{ + // ensure that issuance exists + auto const sleCftIssuance = + ctx.view.read(keylet::cftIssuance(ctx.tx[sfCFTokenIssuanceID])); + if (!sleCftIssuance) + return tecOBJECT_NOT_FOUND; + + // if the cft has disabled locking + if (!((*sleCftIssuance)[sfFlags] & lsfCFTCanLock)) + return tecNO_PERMISSION; + + // ensure it is issued by the tx submitter + if ((*sleCftIssuance)[sfIssuer] != ctx.tx[sfAccount]) + return tecNO_PERMISSION; + + auto const holderID = ctx.tx[~sfCFTokenHolder]; + + // make sure holder account exists + if (holderID && !ctx.view.exists(keylet::account(*holderID))) + return tecNO_DST; + + // the cftoken must exist + if (holderID && + !ctx.view.exists( + keylet::cftoken(ctx.tx[sfCFTokenIssuanceID], *holderID))) + return tecOBJECT_NOT_FOUND; + + return tesSUCCESS; +} + +TER +CFTokenIssuanceSet::doApply() +{ + auto const cftIssuanceID = ctx_.tx[sfCFTokenIssuanceID]; + auto const txFlags = ctx_.tx.getFlags(); + auto const holderID = ctx_.tx[~sfCFTokenHolder]; + std::shared_ptr sle; + + if (holderID) + sle = view().peek(keylet::cftoken(cftIssuanceID, *holderID)); + else + sle = view().peek(keylet::cftIssuance(cftIssuanceID)); + + if (!sle) + return tecINTERNAL; + + std::uint32_t const flagsIn = sle->getFieldU32(sfFlags); + std::uint32_t flagsOut = flagsIn; + + if (txFlags & tfCFTLock) + flagsOut |= lsfCFTLocked; + else if (txFlags & tfCFTUnlock) + flagsOut &= ~lsfCFTLocked; + + if (flagsIn != flagsOut) + sle->setFieldU32(sfFlags, flagsOut); + + view().update(sle); + + return tesSUCCESS; +} + +} // namespace ripple \ No newline at end of file diff --git a/src/ripple/app/tx/impl/CFTokenIssuanceSet.h b/src/ripple/app/tx/impl/CFTokenIssuanceSet.h new file mode 100644 index 00000000000..b17c2de9c6a --- /dev/null +++ b/src/ripple/app/tx/impl/CFTokenIssuanceSet.h @@ -0,0 +1,48 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_CFTOKENISSUANCESET_H_INCLUDED +#define RIPPLE_TX_CFTOKENISSUANCESET_H_INCLUDED + +#include + +namespace ripple { + +class CFTokenIssuanceSet : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit CFTokenIssuanceSet(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/ripple/basics/CFTAmount.h b/src/ripple/basics/CFTAmount.h new file mode 100644 index 00000000000..c9790db6dd2 --- /dev/null +++ b/src/ripple/basics/CFTAmount.h @@ -0,0 +1,247 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2012, 2013 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED +#define RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace ripple { + +class CFTAmount : private boost::totally_ordered, + private boost::additive, + private boost::equality_comparable, + private boost::additive +{ +public: + using cft_type = std::int64_t; + +protected: + cft_type cft_; + +public: + CFTAmount() = default; + constexpr CFTAmount(CFTAmount const& other) = default; + constexpr CFTAmount& + operator=(CFTAmount const& other) = default; + + constexpr CFTAmount(beast::Zero) : cft_(0) + { + } + + constexpr explicit CFTAmount(cft_type value) : cft_(value) + { + } + + constexpr CFTAmount& + operator=(beast::Zero) + { + cft_ = 0; + return *this; + } + + CFTAmount& + operator=(cft_type value) + { + cft_ = value; + return *this; + } + + constexpr CFTAmount + operator*(cft_type const& rhs) const + { + return CFTAmount{cft_ * rhs}; + } + + friend constexpr CFTAmount + operator*(cft_type lhs, CFTAmount const& rhs) + { + // multiplication is commutative + return rhs * lhs; + } + + CFTAmount& + operator+=(CFTAmount const& other) + { + cft_ += other.cft(); + return *this; + } + + CFTAmount& + operator-=(CFTAmount const& other) + { + cft_ -= other.cft(); + return *this; + } + + CFTAmount& + operator+=(cft_type const& rhs) + { + cft_ += rhs; + return *this; + } + + CFTAmount& + operator-=(cft_type const& rhs) + { + cft_ -= rhs; + return *this; + } + + CFTAmount& + operator*=(cft_type const& rhs) + { + cft_ *= rhs; + return *this; + } + + CFTAmount + operator-() const + { + return CFTAmount{-cft_}; + } + + bool + operator==(CFTAmount const& other) const + { + return cft_ == other.cft_; + } + + bool + operator==(cft_type other) const + { + return cft_ == other; + } + + bool + operator<(CFTAmount const& other) const + { + return cft_ < other.cft_; + } + + /** Returns true if the amount is not zero */ + explicit constexpr operator bool() const noexcept + { + return cft_ != 0; + } + + /** Return the sign of the amount */ + constexpr int + signum() const noexcept + { + return (cft_ < 0) ? -1 : (cft_ ? 1 : 0); + } + + Json::Value + jsonClipped() const + { + static_assert( + std::is_signed_v && std::is_integral_v, + "Expected CFTAmount to be a signed integral type"); + + constexpr auto min = std::numeric_limits::min(); + constexpr auto max = std::numeric_limits::max(); + + if (cft_ < min) + return min; + if (cft_ > max) + return max; + return static_cast(cft_); + } + + /** Returns the underlying value. Code SHOULD NOT call this + function unless the type has been abstracted away, + e.g. in a templated function. + */ + constexpr cft_type + cft() const + { + return cft_; + } + + friend std::istream& + operator>>(std::istream& s, CFTAmount& val) + { + s >> val.cft_; + return s; + } + + static CFTAmount + minPositiveAmount() + { + return CFTAmount{1}; + } +}; + +// Output CFTAmount as just the value. +template +std::basic_ostream& +operator<<(std::basic_ostream& os, const CFTAmount& q) +{ + return os << q.cft(); +} + +inline std::string +to_string(CFTAmount const& amount) +{ + return std::to_string(amount.cft()); +} + +inline CFTAmount +mulRatio( + CFTAmount const& amt, + std::uint32_t num, + std::uint32_t den, + bool roundUp) +{ + using namespace boost::multiprecision; + + if (!den) + Throw("division by zero"); + + int128_t const amt128(amt.cft()); + auto const neg = amt.cft() < 0; + auto const m = amt128 * num; + auto r = m / den; + if (m % den) + { + if (!neg && roundUp) + r += 1; + if (neg && !roundUp) + r -= 1; + } + if (r > std::numeric_limits::max()) + Throw("XRP mulRatio overflow"); + return CFTAmount(r.convert_to()); +} + +} // namespace ripple + +#endif // RIPPLE_BASICS_INTEGRALAMOUNT_H_INCLUDED diff --git a/src/test/app/CFToken_test.cpp b/src/test/app/CFToken_test.cpp new file mode 100644 index 00000000000..8c1458727c1 --- /dev/null +++ b/src/test/app/CFToken_test.cpp @@ -0,0 +1,929 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +class CFToken_test : public beast::unit_test::suite +{ + bool + checkCFTokenAmount( + test::jtx::Env const& env, + ripple::uint256 const cftIssuanceid, + test::jtx::Account const& holder, + std::uint64_t expectedAmount) + { + auto const sleCft = env.le(keylet::cftoken(cftIssuanceid, holder)); + if (!sleCft) + return false; + + std::uint64_t const amount = (*sleCft)[sfCFTAmount]; + return amount == expectedAmount; + } + + bool + checkCFTokenIssuanceFlags( + test::jtx::Env const& env, + ripple::uint256 const cftIssuanceid, + uint32_t const expectedFlags) + { + auto const sleCftIssuance = env.le(keylet::cftIssuance(cftIssuanceid)); + if (!sleCftIssuance) + return false; + + uint32_t const cftIssuanceFlags = sleCftIssuance->getFlags(); + return expectedFlags == cftIssuanceFlags; + } + + bool + checkCFTokenFlags( + test::jtx::Env const& env, + ripple::uint256 const cftIssuanceid, + test::jtx::Account const& holder, + uint32_t const expectedFlags) + { + auto const sleCft = env.le(keylet::cftoken(cftIssuanceid, holder)); + if (!sleCft) + return false; + uint32_t const cftFlags = sleCft->getFlags(); + return cftFlags == expectedFlags; + } + + void + testCreateValidation(FeatureBitset features) + { + testcase("Create Validate"); + using namespace test::jtx; + + // test preflight of CFTokenIssuanceCreate + { + // If the CFT amendment is not enabled, you should not be able to + // create CFTokenIssuances + Env env{*this, features - featureCFTokensV1}; + Account const alice("alice"); // issuer + + env.fund(XRP(10000), alice); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + env(cft::create(alice), ter(temDISABLED)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + env.enableFeature(featureCFTokensV1); + + env(cft::create(alice), txflags(0x00000001), ter(temINVALID_FLAG)); + env.close(); + + // tries to set a txfee while not enabling in the flag + env(cft::create(alice, 100, 0, 1, "test"), ter(temMALFORMED)); + env.close(); + + // tries to set a txfee while not enabling transfer + env(cft::create(alice, 100, 0, maxTransferFee + 1, "test"), + txflags(tfCFTCanTransfer), + ter(temBAD_CFTOKEN_TRANSFER_FEE)); + env.close(); + + // empty metadata returns error + env(cft::create(alice, 100, 0, 0, ""), ter(temMALFORMED)); + env.close(); + } + } + + void + testCreateEnabled(FeatureBitset features) + { + testcase("Create Enabled"); + + using namespace test::jtx; + + { + // If the CFT amendment IS enabled, you should be able to create + // CFTokenIssuances + Env env{*this, features}; + Account const alice("alice"); // issuer + + env.fund(XRP(10000), alice); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice, 100, 1, 10, "123"), + txflags( + tfCFTCanLock | tfCFTRequireAuth | tfCFTCanEscrow | + tfCFTCanTrade | tfCFTCanTransfer | tfCFTCanClawback)); + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags( + env, + id.key, + lsfCFTCanLock | lsfCFTRequireAuth | lsfCFTCanEscrow | + lsfCFTCanTrade | lsfCFTCanTransfer | lsfCFTCanClawback)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + } + } + + void + testDestroyValidation(FeatureBitset features) + { + testcase("Destroy Validate"); + + using namespace test::jtx; + // CFTokenIssuanceDestroy (preflight) + { + Env env{*this, features - featureCFTokensV1}; + Account const alice("alice"); // issuer + + env.fund(XRP(10000), alice); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::destroy(alice, id.key), ter(temDISABLED)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + env.enableFeature(featureCFTokensV1); + + env(cft::destroy(alice, id.key), + txflags(0x00000001), + ter(temINVALID_FLAG)); + env.close(); + } + + // CFTokenIssuanceDestroy (preclaim) + { + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const fakeID = keylet::cftIssuance(alice.id(), env.seq(alice)); + + env(cft::destroy(alice, fakeID.key), ter(tecOBJECT_NOT_FOUND)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // a non-issuer tries to destroy a cftissuance they didn't issue + env(cft::destroy(bob, id.key), ter(tecNO_PERMISSION)); + env.close(); + + // TODO: add test when OutstandingAmount is non zero + } + } + + void + testDestroyEnabled(FeatureBitset features) + { + testcase("Destroy Enabled"); + + using namespace test::jtx; + + // If the CFT amendment IS enabled, you should be able to destroy + // CFTokenIssuances + Env env{*this, features}; + Account const alice("alice"); // issuer + + env.fund(XRP(10000), alice); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + env(cft::destroy(alice, id.key)); + env.close(); + BEAST_EXPECT(env.ownerCount(alice) == 0); + } + + void + testAuthorizeValidation(FeatureBitset features) + { + testcase("Validate authorize transaction"); + + using namespace test::jtx; + // Validate fields in CFTokenAuthorize (preflight) + { + Env env{*this, features - featureCFTokensV1}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + + env(cft::authorize(bob, id.key, std::nullopt), ter(temDISABLED)); + env.close(); + + env.enableFeature(featureCFTokensV1); + + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + env(cft::authorize(bob, id.key, std::nullopt), + txflags(0x00000002), + ter(temINVALID_FLAG)); + env.close(); + + env(cft::authorize(bob, id.key, bob), ter(temMALFORMED)); + env.close(); + + env(cft::authorize(alice, id.key, alice), ter(temMALFORMED)); + env.close(); + } + + // Try authorizing when CFTokenIssuance doesnt exist in CFTokenAuthorize + // (preclaim) + { + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + + env(cft::authorize(alice, id.key, bob), ter(tecOBJECT_NOT_FOUND)); + env.close(); + + env(cft::authorize(bob, id.key, std::nullopt), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + } + + // Test bad scenarios without allowlisting in CFTokenAuthorize + // (preclaim) + { + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, 0)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // bob submits a tx with a holder field + env(cft::authorize(bob, id.key, alice), ter(temMALFORMED)); + env.close(); + + env(cft::authorize(bob, id.key, bob), ter(temMALFORMED)); + env.close(); + + env(cft::authorize(alice, id.key, alice), ter(temMALFORMED)); + env.close(); + + // the cft does not enable allowlisting + env(cft::authorize(alice, id.key, bob), ter(tecNO_AUTH)); + env.close(); + + // bob now holds a cftoken object + env(cft::authorize(bob, id.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + + // bob cannot create the cftoken the second time + env(cft::authorize(bob, id.key, std::nullopt), + ter(tecCFTOKEN_EXISTS)); + env.close(); + + // TODO: check where cftoken balance is nonzero + + env(cft::authorize(bob, id.key, std::nullopt), + txflags(tfCFTUnauthorize)); + env.close(); + + env(cft::authorize(bob, id.key, std::nullopt), + txflags(tfCFTUnauthorize), + ter(tecNO_ENTRY)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 0); + } + + // Test bad scenarios with allow-listing in CFTokenAuthorize (preclaim) + { + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const cindy("cindy"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice), txflags(tfCFTRequireAuth)); + env.close(); + + BEAST_EXPECT( + checkCFTokenIssuanceFlags(env, id.key, lsfCFTRequireAuth)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // alice submits a tx without specifying a holder's account + env(cft::authorize(alice, id.key, std::nullopt), ter(temMALFORMED)); + env.close(); + + // alice submits a tx to authorize a holder that hasn't created a + // cftoken yet + env(cft::authorize(alice, id.key, bob), ter(tecNO_ENTRY)); + env.close(); + + // alice specifys a holder acct that doesn't exist + env(cft::authorize(alice, id.key, cindy), ter(tecNO_DST)); + env.close(); + + // bob now holds a cftoken object + env(cft::authorize(bob, id.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + + // alice tries to unauthorize bob. + // although tx is successful, + // but nothing happens because bob hasn't been authorized yet + env(cft::authorize(alice, id.key, bob), txflags(tfCFTUnauthorize)); + env.close(); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + + // alice authorizes bob + // make sure bob's cftoken has set lsfCFTAuthorized + env(cft::authorize(alice, id.key, bob)); + env.close(); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTAuthorized)); + + // alice tries authorizes bob again. + // tx is successful, but bob is already authorized, + // so no changes + env(cft::authorize(alice, id.key, bob)); + env.close(); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTAuthorized)); + + // bob deletes his cftoken + env(cft::authorize(bob, id.key, std::nullopt), + txflags(tfCFTUnauthorize)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 0); + } + + // Test cftoken reserve requirement - first two cfts free (doApply) + { + Env env{*this, features}; + auto const acctReserve = env.current()->fees().accountReserve(0); + auto const incReserve = env.current()->fees().increment; + + Account const alice("alice"); + Account const bob("bob"); + + env.fund(XRP(10000), alice); + env.fund(acctReserve + XRP(1), bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id1 = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice)); + env.close(); + + auto const id2 = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice)); + env.close(); + + auto const id3 = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 3); + + // first cft for free + env(cft::authorize(bob, id1.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + + // second cft free + env(cft::authorize(bob, id2.key, std::nullopt)); + env.close(); + BEAST_EXPECT(env.ownerCount(bob) == 2); + + env(cft::authorize(bob, id3.key, std::nullopt), + ter(tecINSUFFICIENT_RESERVE)); + env.close(); + + env(pay( + env.master, bob, drops(incReserve + incReserve + incReserve))); + env.close(); + + env(cft::authorize(bob, id3.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 3); + } + } + + void + testAuthorizeEnabled(FeatureBitset features) + { + testcase("Authorize Enabled"); + + using namespace test::jtx; + // Basic authorization without allowlisting + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + // alice create cftissuance without allowisting + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, 0)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // bob creates a cftoken + env(cft::authorize(bob, id.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + BEAST_EXPECT(checkCFTokenAmount(env, id.key, bob, 0)); + + // bob deletes his cftoken + env(cft::authorize(bob, id.key, std::nullopt), + txflags(tfCFTUnauthorize)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 0); + } + + // With allowlisting + { + Env env{*this, features}; + Account const alice("alice"); + Account const bob("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + // alice creates a cftokenissuance that requires authorization + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + env(cft::create(alice), txflags(tfCFTRequireAuth)); + env.close(); + + BEAST_EXPECT( + checkCFTokenIssuanceFlags(env, id.key, lsfCFTRequireAuth)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // bob creates a cftoken + env(cft::authorize(bob, id.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + BEAST_EXPECT(checkCFTokenAmount(env, id.key, bob, 0)); + + // alice authorizes bob + env(cft::authorize(alice, id.key, bob)); + env.close(); + + // make sure bob's cftoken has lsfCFTAuthorized set + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTAuthorized)); + + // Unauthorize bob's cftoken + env(cft::authorize(alice, id.key, bob), txflags(tfCFTUnauthorize)); + env.close(); + + // ensure bob's cftoken no longer has lsfCFTAuthorized set + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + + env(cft::authorize(bob, id.key, std::nullopt), + txflags(tfCFTUnauthorize)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 0); + } + + // TODO: test allowlisting cases where bob tries to send tokens + // without being authorized. + } + + void + testSetValidation(FeatureBitset features) + { + testcase("Validate set transaction"); + + using namespace test::jtx; + // Validate fields in CFTokenIssuanceSet (preflight) + { + Env env{*this, features - featureCFTokensV1}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + + env(cft::set(bob, id.key, std::nullopt), ter(temDISABLED)); + env.close(); + + env.enableFeature(featureCFTokensV1); + + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, 0)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + BEAST_EXPECT(env.ownerCount(bob) == 0); + + env(cft::authorize(bob, id.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + + // test invalid flag + env(cft::set(alice, id.key, std::nullopt), + txflags(0x00000008), + ter(temINVALID_FLAG)); + env.close(); + + // set both lock and unlock flags at the same time will fail + env(cft::set(alice, id.key, std::nullopt), + txflags(tfCFTLock | tfCFTUnlock), + ter(temINVALID_FLAG)); + env.close(); + + // if the holder is the same as the acct that submitted the tx, tx + // fails + env(cft::set(alice, id.key, alice), + txflags(tfCFTLock), + ter(temMALFORMED)); + env.close(); + } + + // Validate fields in CFTokenIssuanceSet (preclaim) + // test when a cftokenissuance has disabled locking + { + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const cindy("cindy"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + + env(cft::create(alice)); // no locking + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, 0)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // alice tries to lock a cftissuance that has disabled locking + env(cft::set(alice, id.key, std::nullopt), + txflags(tfCFTLock), + ter(tecNO_PERMISSION)); + env.close(); + + // alice tries to unlock cftissuance that has disabled locking + env(cft::set(alice, id.key, std::nullopt), + txflags(tfCFTUnlock), + ter(tecNO_PERMISSION)); + env.close(); + + // issuer tries to lock a bob's cftoken that has disabled locking + env(cft::set(alice, id.key, bob), + txflags(tfCFTLock), + ter(tecNO_PERMISSION)); + env.close(); + + // issuer tries to unlock a bob's cftoken that has disabled locking + env(cft::set(alice, id.key, bob), + txflags(tfCFTUnlock), + ter(tecNO_PERMISSION)); + env.close(); + } + + // Validate fields in CFTokenIssuanceSet (preclaim) + // test when cftokenissuance has enabled locking + { + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + Account const cindy("cindy"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const badID = keylet::cftIssuance(alice.id(), env.seq(alice)); + + // alice trying to set when the cftissuance doesn't exist yet + env(cft::set(alice, badID.key, std::nullopt), + txflags(tfCFTLock), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + + // create a cftokenissuance with locking + env(cft::create(alice), txflags(tfCFTCanLock)); + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + + // a non-issuer acct tries to set the cftissuance + env(cft::set(bob, id.key, std::nullopt), + txflags(tfCFTLock), + ter(tecNO_PERMISSION)); + env.close(); + + // trying to set a holder who doesn't have a cftoken + env(cft::set(alice, id.key, bob), + txflags(tfCFTLock), + ter(tecOBJECT_NOT_FOUND)); + env.close(); + + // trying to set a holder who doesn't exist + env(cft::set(alice, id.key, cindy), + txflags(tfCFTLock), + ter(tecNO_DST)); + env.close(); + } + } + + void + testSetEnabled(FeatureBitset features) + { + testcase("Enabled set transaction"); + + using namespace test::jtx; + + // Test locking and unlocking + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + + // create a cftokenissuance with locking + env(cft::create(alice), txflags(tfCFTCanLock)); + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + BEAST_EXPECT(env.ownerCount(bob) == 0); + + env(cft::authorize(bob, id.key, std::nullopt)); + env.close(); + + BEAST_EXPECT(env.ownerCount(bob) == 1); + env.close(); + + // both the cftissuance and cftoken are not locked + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + + // locks bob's cftoken + env(cft::set(alice, id.key, bob), txflags(tfCFTLock)); + env.close(); + + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTLocked)); + + // trying to lock bob's cftoken again will still succeed + // but no changes to the objects + env(cft::set(alice, id.key, bob), txflags(tfCFTLock)); + env.close(); + + // no changes to the objects + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTLocked)); + + // alice locks the cftissuance + env(cft::set(alice, id.key, std::nullopt), txflags(tfCFTLock)); + env.close(); + + // now both the cftissuance and cftoken are locked up + BEAST_EXPECT(checkCFTokenIssuanceFlags( + env, id.key, lsfCFTCanLock | lsfCFTLocked)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTLocked)); + + // alice tries to lock up both cftissuance and cftoken again + // it will not change the flags and both will remain locked. + env(cft::set(alice, id.key, std::nullopt), txflags(tfCFTLock)); + env.close(); + env(cft::set(alice, id.key, bob), txflags(tfCFTLock)); + env.close(); + + // now both the cftissuance and cftoken remain locked up + BEAST_EXPECT(checkCFTokenIssuanceFlags( + env, id.key, lsfCFTCanLock | lsfCFTLocked)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTLocked)); + + // alice unlocks bob's cftoken + env(cft::set(alice, id.key, bob), txflags(tfCFTUnlock)); + env.close(); + + // only cftissuance is locked + BEAST_EXPECT(checkCFTokenIssuanceFlags( + env, id.key, lsfCFTCanLock | lsfCFTLocked)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + + // locks up bob's cftoken again + env(cft::set(alice, id.key, bob), txflags(tfCFTLock)); + env.close(); + + // now both the cftissuance and cftokens are locked up + BEAST_EXPECT(checkCFTokenIssuanceFlags( + env, id.key, lsfCFTCanLock | lsfCFTLocked)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTLocked)); + + // alice unlocks cftissuance + env(cft::set(alice, id.key, std::nullopt), txflags(tfCFTUnlock)); + env.close(); + + // now cftissuance is unlocked + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, lsfCFTLocked)); + + // alice unlocks bob's cftoken + env(cft::set(alice, id.key, bob), txflags(tfCFTUnlock)); + env.close(); + + // both cftissuance and bob's cftoken are unlocked + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + + // alice unlocks cftissuance and bob's cftoken again despite that + // they are already unlocked. Make sure this will not change the + // flags + env(cft::set(alice, id.key, bob), txflags(tfCFTUnlock)); + env.close(); + env(cft::set(alice, id.key, std::nullopt), txflags(tfCFTUnlock)); + env.close(); + + // both cftissuance and bob's cftoken remain unlocked + BEAST_EXPECT(checkCFTokenIssuanceFlags(env, id.key, lsfCFTCanLock)); + BEAST_EXPECT(checkCFTokenFlags(env, id.key, bob, 0)); + } + + void + testPayment(FeatureBitset features) + { + testcase("Payment"); + + using namespace test::jtx; + { + Env env{*this, features}; + Account const alice("alice"); // issuer + Account const bob("bob"); // holder + + env.fund(XRP(10000), alice, bob); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 0); + + auto const id = keylet::cftIssuance(alice.id(), env.seq(alice)); + + env(cft::create(alice)); + env.close(); + + BEAST_EXPECT(env.ownerCount(alice) == 1); + BEAST_EXPECT(env.ownerCount(bob) == 0); + + // env(cft::authorize(alice, id.key, std::nullopt)); + // env.close(); + + env(cft::authorize(bob, id.key, std::nullopt)); + env.close(); + + env(pay(alice, bob, CFT(alice, id.key)(100))); + env.close(); + BEAST_EXPECT(checkCFTokenAmount(env, id.key, bob, 100)); + } + } + +public: + void + run() override + { + using namespace test::jtx; + FeatureBitset const all{supported_amendments()}; + + // CFTokenIssuanceCreate + testCreateValidation(all); + testCreateEnabled(all); + + // CFTokenIssuanceDestroy + testDestroyValidation(all); + testDestroyEnabled(all); + + // CFTokenAuthorize + testAuthorizeValidation(all); + testAuthorizeEnabled(all); + + // CFTokenIssuanceSet + testSetValidation(all); + testSetEnabled(all); + + testPayment(all); + } +}; + +BEAST_DEFINE_TESTSUITE_PRIO(CFToken, tx, ripple, 2); + +} // namespace ripple diff --git a/src/test/jtx/cft.h b/src/test/jtx/cft.h new file mode 100644 index 00000000000..7f586e14641 --- /dev/null +++ b/src/test/jtx/cft.h @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_CFT_H_INCLUDED +#define RIPPLE_TEST_JTX_CFT_H_INCLUDED + +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace cft { + +/** Issue a CFT with default fields. */ +Json::Value +create(jtx::Account const& account); + +/** Issue a CFT with user-defined fields. */ +Json::Value +create( + jtx::Account const& account, + std::uint32_t const maxAmt, + std::uint8_t const assetScale, + std::uint16_t transferFee, + std::string metadata); + +/** Destroy a CFT. */ +Json::Value +destroy(jtx::Account const& account, ripple::uint256 const& id); + +/** Authorize a CFT. */ +Json::Value +authorize( + jtx::Account const& account, + ripple::uint256 const& issuanceID, + std::optional const& holder); + +/** Set a CFT. */ +Json::Value +set(jtx::Account const& account, + ripple::uint256 const& issuanceID, + std::optional const& holder); +} // namespace cft + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/cft.cpp b/src/test/jtx/impl/cft.cpp new file mode 100644 index 00000000000..6b3141f3f1a --- /dev/null +++ b/src/test/jtx/impl/cft.cpp @@ -0,0 +1,102 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace cft { + +Json::Value +create(jtx::Account const& account) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfTransactionType.jsonName] = jss::CFTokenIssuanceCreate; + return jv; +} + +Json::Value +create( + jtx::Account const& account, + std::uint32_t const maxAmt, + std::uint8_t const assetScale, + std::uint16_t transferFee, + std::string metadata) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfTransactionType.jsonName] = jss::CFTokenIssuanceCreate; + jv[sfMaximumAmount.jsonName] = maxAmt; + jv[sfAssetScale.jsonName] = assetScale; + jv[sfTransferFee.jsonName] = transferFee; + jv[sfCFTokenMetadata.jsonName] = strHex(metadata); + return jv; +} + +Json::Value +destroy(jtx::Account const& account, ripple::uint256 const& id) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfCFTokenIssuanceID.jsonName] = to_string(id); + jv[sfTransactionType.jsonName] = jss::CFTokenIssuanceDestroy; + return jv; +} + +Json::Value +authorize( + jtx::Account const& account, + ripple::uint256 const& issuanceID, + std::optional const& holder) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfTransactionType.jsonName] = jss::CFTokenAuthorize; + jv[sfCFTokenIssuanceID.jsonName] = to_string(issuanceID); + if (holder) + jv[sfCFTokenHolder.jsonName] = holder->human(); + + return jv; +} + +Json::Value +set(jtx::Account const& account, + ripple::uint256 const& issuanceID, + std::optional const& holder) +{ + Json::Value jv; + jv[sfAccount.jsonName] = account.human(); + jv[sfTransactionType.jsonName] = jss::CFTokenIssuanceSet; + jv[sfCFTokenIssuanceID.jsonName] = to_string(issuanceID); + if (holder) + jv[sfCFTokenHolder.jsonName] = holder->human(); + + return jv; +} + +} // namespace cft + +} // namespace jtx +} // namespace test +} // namespace ripple