-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat: Make VaultID conditional on LoanBrokerSet #6528
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: tapanito/lending-fix-amendment
Are you sure you want to change the base?
Changes from all commits
23eee61
3314c21
df1a55d
73fe6e1
20d6e93
929d15b
5c414eb
3c41d29
b921570
64c7ff9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,19 @@ | ||
| #include <xrpl/tx/transactors/lending/LoanBrokerSet.h> | ||
|
|
||
| #include <xrpl/basics/Expected.h> | ||
| #include <xrpl/basics/Log.h> | ||
| #include <xrpl/basics/Number.h> | ||
| #include <xrpl/basics/base_uint.h> | ||
| #include <xrpl/beast/utility/Zero.h> | ||
| #include <xrpl/beast/utility/instrumentation.h> | ||
| #include <xrpl/core/ServiceRegistry.h> | ||
| #include <xrpl/ledger/View.h> | ||
| #include <xrpl/ledger/helpers/AccountRootHelpers.h> | ||
| #include <xrpl/ledger/helpers/LendingHelpers.h> | ||
| #include <xrpl/ledger/helpers/TokenHelpers.h> | ||
| #include <xrpl/protocol/AccountID.h> | ||
| #include <xrpl/protocol/Asset.h> | ||
| #include <xrpl/protocol/Feature.h> | ||
| #include <xrpl/protocol/Indexes.h> | ||
| #include <xrpl/protocol/Protocol.h> | ||
| #include <xrpl/protocol/SField.h> | ||
|
|
@@ -49,7 +54,9 @@ | |
| if (!validNumericRange(tx[~sfDebtMaximum], Number(kMaxMpTokenAmount), Number(0))) | ||
| return temINVALID; | ||
|
|
||
| if (tx.isFieldPresent(sfLoanBrokerID)) | ||
| auto const isLoanBrokerUpdate = tx.isFieldPresent(sfLoanBrokerID); | ||
|
|
||
| if (isLoanBrokerUpdate) | ||
| { | ||
| // Fixed fields can not be specified if we're modifying an existing | ||
| // LoanBroker Object | ||
|
|
@@ -61,9 +68,27 @@ | |
| return temINVALID; | ||
| } | ||
|
|
||
| if (auto const vaultID = tx.at(~sfVaultID)) | ||
| // Amendment-specific field presence rules | ||
| if (ctx.rules.enabled(featureLendingProtocolV1_1)) | ||
| { | ||
| if (isLoanBrokerUpdate) | ||
| { | ||
| if (tx.isFieldPresent(sfVaultID)) | ||
| return temINVALID; | ||
| } | ||
| else | ||
| { | ||
| if (!tx.isFieldPresent(sfVaultID) || tx[sfVaultID] == beast::kZero) | ||
| return temINVALID; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| if (*vaultID == beast::kZero) | ||
| // Pre-amendment: VaultID was soeREQUIRED, must always be present | ||
| if (!tx.isFieldPresent(sfVaultID)) | ||
| return temINVALID; | ||
|
|
||
| if (tx[sfVaultID] == beast::kZero) | ||
| return temINVALID; | ||
| } | ||
|
|
||
|
|
@@ -88,77 +113,157 @@ | |
| return kValueFields; | ||
| } | ||
|
|
||
| TER | ||
| LoanBrokerSet::preclaim(PreclaimContext const& ctx) | ||
| /** Read and validate a vault, checking existence and ownership. | ||
| * | ||
| * @param ctx The preclaim context. | ||
| * @param account The expected vault owner. | ||
| * @param id The vault ID to look up. | ||
| * @return The vault SLE on success, or a TER error. | ||
| */ | ||
| [[nodiscard]] static Expected<std::shared_ptr<SLE const>, TER> | ||
| readVault(PreclaimContext const& ctx, AccountID const& account, uint256 const& id) | ||
| { | ||
| auto const& tx = ctx.tx; | ||
|
|
||
| auto const account = tx[sfAccount]; | ||
| auto const vaultID = tx[sfVaultID]; | ||
|
|
||
| auto const sleVault = ctx.view.read(keylet::vault(vaultID)); | ||
| if (!sleVault) | ||
| auto const sle = ctx.view.read(keylet::vault(id)); | ||
| if (!sle) | ||
| { | ||
| JLOG(ctx.j.warn()) << "Vault does not exist."; | ||
| return tecNO_ENTRY; | ||
| return Unexpected(tecNO_ENTRY); | ||
| } | ||
| Asset const asset = sleVault->at(sfAsset); | ||
|
|
||
| if (account != sleVault->at(sfOwner)) | ||
| if (account != sle->at(sfOwner)) | ||
| { | ||
| JLOG(ctx.j.warn()) << "Account is not the owner of the Vault."; | ||
| return tecNO_PERMISSION; | ||
| return Unexpected(tecNO_PERMISSION); | ||
| } | ||
| return sle; | ||
| } | ||
|
|
||
| if (auto const brokerID = tx[~sfLoanBrokerID]) | ||
| { | ||
| // Updating an existing Broker | ||
| /** Preclaim validation for updating an existing LoanBroker. | ||
| * | ||
| * @param ctx The preclaim context. | ||
| * @param account The transaction submitter. | ||
| * @param brokerID The LoanBroker ID to update. | ||
| * @return The vault SLE on success, or a TER error. | ||
| */ | ||
| [[nodiscard]] static Expected<std::shared_ptr<SLE const>, TER> | ||
| preclaimUpdate(PreclaimContext const& ctx, AccountID const& account, uint256 const& brokerID) | ||
|
Tapanito marked this conversation as resolved.
|
||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inconsistent ownership validation order in post-amendment update path In Suggested fix: Add an explicit assertion or comment documenting that the vault owner and broker owner are expected to be identical (invariant established at creation time). Consider also adding a check that |
||
| auto const& tx = ctx.tx; | ||
| bool const fixEnabled = ctx.view.rules().enabled(featureLendingProtocolV1_1); | ||
|
|
||
| auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID)); | ||
| std::shared_ptr<SLE const> sleBroker; | ||
| std::shared_ptr<SLE const> sleVault; | ||
|
|
||
| if (fixEnabled) | ||
| { | ||
| // Post-amendment: VaultID is not in the tx, read it from broker | ||
| sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); | ||
| if (!sleBroker) | ||
| { | ||
| JLOG(ctx.j.warn()) << "LoanBroker does not exist."; | ||
| return tecNO_ENTRY; | ||
| return Unexpected(tecNO_ENTRY); | ||
| } | ||
| if (vaultID != sleBroker->at(sfVaultID)) | ||
| { | ||
| JLOG(ctx.j.warn()) << "Can not change VaultID on an existing LoanBroker."; | ||
| return tecNO_PERMISSION; | ||
| } | ||
| if (account != sleBroker->at(sfOwner)) | ||
|
|
||
| auto const vault = readVault(ctx, account, sleBroker->at(sfVaultID)); | ||
| if (!vault) | ||
| return vault; | ||
| sleVault = *vault; | ||
| } | ||
| else | ||
| { | ||
| XRPL_ASSERT( | ||
| tx.isFieldPresent(sfVaultID), | ||
| "xrpl::LoanBrokerSet::preclaimUpdate : VaultID is present in the transaction"); | ||
| // Pre-amendment: vault is validated before broker to preserve | ||
| // the original error ordering for historical transaction replay. | ||
| auto const vault = readVault(ctx, account, tx[sfVaultID]); | ||
|
Tapanito marked this conversation as resolved.
|
||
| if (!vault) | ||
| return vault; | ||
| sleVault = *vault; | ||
|
|
||
| sleBroker = ctx.view.read(keylet::loanbroker(brokerID)); | ||
| if (!sleBroker) | ||
| { | ||
| JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; | ||
| return tecNO_PERMISSION; | ||
| JLOG(ctx.j.warn()) << "LoanBroker does not exist."; | ||
| return Unexpected(tecNO_ENTRY); | ||
| } | ||
|
|
||
| if (auto const debtMax = tx[~sfDebtMaximum]) | ||
| if (tx[sfVaultID] != sleBroker->at(sfVaultID)) | ||
|
Tapanito marked this conversation as resolved.
|
||
| { | ||
| // Can't reduce the debt maximum below the current total debt | ||
| auto const currentDebtTotal = sleBroker->at(sfDebtTotal); | ||
| if (*debtMax != 0 && *debtMax < currentDebtTotal) | ||
| { | ||
| JLOG(ctx.j.warn()) << "Cannot reduce DebtMaximum below current DebtTotal."; | ||
| return tecLIMIT_EXCEEDED; | ||
| } | ||
| JLOG(ctx.j.warn()) << "Can not change VaultID on an existing LoanBroker."; | ||
| return Unexpected(tecNO_PERMISSION); | ||
| } | ||
| } | ||
| else | ||
|
|
||
| XRPL_ASSERT(sleVault, "xrpl::LoanBrokerSet::preclaimUpdate : sleVault is initialized"); | ||
|
|
||
| if (account != sleBroker->at(sfOwner)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Vault owner and broker owner are checked independently — add a defensive cross-validation. In the // Defensive invariant: vault owner and broker owner must agree
if (sleVault->at(sfOwner) != sleBroker->at(sfOwner))
{
JLOG(ctx.j.warn()) << "Vault and LoanBroker owner mismatch (ledger corruption?)";
return Unexpected(tefINTERNAL);
}Insert this block before the existing |
||
| { | ||
| if (auto const ter = canAddHolding(ctx.view, asset)) | ||
| return ter; | ||
| JLOG(ctx.j.warn()) << "Account is not the owner of the LoanBroker."; | ||
| return Unexpected(tecNO_PERMISSION); | ||
| } | ||
|
|
||
| if (auto const ter = checkFrozen(ctx.view, sleVault->at(sfAccount), sleVault->at(sfAsset))) | ||
| if (auto const debtMax = tx[~sfDebtMaximum]) | ||
| { | ||
| auto const currentDebtTotal = sleBroker->at(sfDebtTotal); | ||
| if (*debtMax != 0 && *debtMax < currentDebtTotal) | ||
| { | ||
| JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; | ||
| return ter; | ||
| JLOG(ctx.j.warn()) << "Cannot reduce DebtMaximum below current DebtTotal."; | ||
| return Unexpected(tecLIMIT_EXCEEDED); | ||
| } | ||
| } | ||
|
|
||
| return sleVault; | ||
| } | ||
|
|
||
| /** Preclaim validation for creating a new LoanBroker. | ||
| * | ||
| * @param ctx The preclaim context. | ||
| * @param account The transaction submitter (vault owner). | ||
| * @return The vault SLE on success, or a TER error. | ||
| */ | ||
| [[nodiscard]] static Expected<std::shared_ptr<SLE const>, TER> | ||
| preclaimCreate(PreclaimContext const& ctx, AccountID const& account) | ||
| { | ||
| XRPL_ASSERT( | ||
| ctx.tx.isFieldPresent(sfVaultID), | ||
| "xrpl::LoanBrokerSet::preclaimCreate : VaultID is present in the transaction"); | ||
| auto const vault = readVault(ctx, account, ctx.tx[sfVaultID]); | ||
|
Tapanito marked this conversation as resolved.
|
||
| if (!vault) | ||
| return vault; | ||
| auto const& sleVault = *vault; | ||
|
|
||
| Asset const asset = sleVault->at(sfAsset); | ||
| if (auto const ter = canAddHolding(ctx.view, asset)) | ||
| return Unexpected(ter); | ||
|
|
||
| if (auto const ter = checkFrozen(ctx.view, sleVault->at(sfAccount), sleVault->at(sfAsset))) | ||
| { | ||
| JLOG(ctx.j.warn()) << "Vault pseudo-account is frozen."; | ||
| return Unexpected(ter); | ||
| } | ||
|
|
||
| return sleVault; | ||
| } | ||
|
|
||
| TER | ||
| LoanBrokerSet::preclaim(PreclaimContext const& ctx) | ||
| { | ||
| auto const account = ctx.tx[sfAccount]; | ||
|
|
||
| auto const maybeVault = [&]() -> Expected<std::shared_ptr<SLE const>, TER> { | ||
| if (auto const brokerID = ctx.tx[~sfLoanBrokerID]) | ||
| return preclaimUpdate(ctx, account, *brokerID); | ||
| return preclaimCreate(ctx, account); | ||
| }(); | ||
|
|
||
| if (!maybeVault) | ||
| return maybeVault.error(); | ||
|
|
||
| // Check that relevant values can be represented as the vault asset | ||
| // type. This is mostly only relevant for integral (non-IOU) types | ||
| // type. This is mostly only relevant for integral (non-IOU) types. | ||
| Asset const asset = (*maybeVault)->at(sfAsset); | ||
| for (auto const& field : getValueFields()) | ||
| { | ||
| if (auto const value = tx[field]; value && STAmount{asset, *value} != *value) | ||
| if (auto const value = ctx.tx[field]; value && STAmount{asset, *value} != *value) | ||
| { | ||
| JLOG(ctx.j.warn()) << field.f->getName() << " (" << *value | ||
| << ") can not be represented as a(n) " << to_string(asset) << "."; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No explicit zero-value guard for
sfLoanBrokerIDin the update path — confirm this is covered elsewhere.The
isLoanBrokerUpdateflag is set whensfLoanBrokerIDis present, but a zerouint256value would pass this check and route intopreclaimUpdate, callingctx.view.read(keylet::loanbroker(uint256{}))on an unintended ledger key. Line 59 guardssfVaultIDagainstbeast::zeroexplicitly —sfLoanBrokerIDdeserves the same treatment. If STTx deserialization already rejects a zero field value, add a comment documenting that invariant; otherwise add an explicit guard:Consider adding above this line: