Files
rippled/src/libxrpl/tx/transactors/AMM/AMMVote.cpp
2026-03-03 20:23:22 +00:00

213 lines
7.3 KiB
C++

#include <xrpl/ledger/Sandbox.h>
#include <xrpl/protocol/AMMCore.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/tx/transactors/AMM/AMMUtils.h>
#include <xrpl/tx/transactors/AMM/AMMVote.h>
namespace xrpl {
bool
AMMVote::checkExtraFeatures(PreflightContext const& ctx)
{
return ammEnabled(ctx.rules);
}
NotTEC
AMMVote::preflight(PreflightContext const& ctx)
{
if (auto const res =
invalidAMMAssetPair(ctx.tx[sfAsset].get<Issue>(), ctx.tx[sfAsset2].get<Issue>()))
{
JLOG(ctx.j.debug()) << "AMM Vote: invalid asset pair.";
return res;
}
if (ctx.tx[sfTradingFee] > TRADING_FEE_THRESHOLD)
{
JLOG(ctx.j.debug()) << "AMM Vote: invalid trading fee.";
return temBAD_FEE;
}
return tesSUCCESS;
}
TER
AMMVote::preclaim(PreclaimContext const& ctx)
{
if (auto const ammSle = ctx.view.read(keylet::amm(ctx.tx[sfAsset], ctx.tx[sfAsset2])); !ammSle)
{
JLOG(ctx.j.debug()) << "AMM Vote: Invalid asset pair.";
return terNO_AMM;
}
else if (ammSle->getFieldAmount(sfLPTokenBalance) == beast::zero)
return tecAMM_EMPTY;
else if (
auto const lpTokensNew = ammLPHolds(ctx.view, *ammSle, ctx.tx[sfAccount], ctx.j);
lpTokensNew == beast::zero)
{
JLOG(ctx.j.debug()) << "AMM Vote: account is not LP.";
return tecAMM_INVALID_TOKENS;
}
return tesSUCCESS;
}
static std::pair<TER, bool>
applyVote(ApplyContext& ctx_, Sandbox& sb, AccountID const& account_, beast::Journal j_)
{
auto const feeNew = ctx_.tx[sfTradingFee];
auto ammSle = sb.peek(keylet::amm(ctx_.tx[sfAsset], ctx_.tx[sfAsset2]));
if (!ammSle)
return {tecINTERNAL, false};
STAmount const lptAMMBalance = (*ammSle)[sfLPTokenBalance];
auto const lpTokensNew = ammLPHolds(sb, *ammSle, account_, ctx_.journal);
std::optional<STAmount> minTokens;
std::size_t minPos{0};
AccountID minAccount{0};
std::uint32_t minFee{0};
STArray updatedVoteSlots;
Number num{0};
Number den{0};
// Account already has vote entry
bool foundAccount = false;
// Iterate over the current vote entries and update each entry
// per current total tokens balance and each LP tokens balance.
// Find the entry with the least tokens and whether the account
// has the vote entry.
for (auto const& entry : ammSle->getFieldArray(sfVoteSlots))
{
auto const account = entry[sfAccount];
auto lpTokens = ammLPHolds(sb, *ammSle, account, ctx_.journal);
if (lpTokens == beast::zero)
{
JLOG(j_.debug()) << "AMMVote::applyVote, account " << account << " is not LP";
continue;
}
auto feeVal = entry[sfTradingFee];
STObject newEntry = STObject::makeInnerObject(sfVoteEntry);
// The account already has the vote entry.
if (account == account_)
{
lpTokens = lpTokensNew;
feeVal = feeNew;
foundAccount = true;
}
// Keep running numerator/denominator to calculate the updated fee.
num += feeVal * lpTokens;
den += lpTokens;
newEntry.setAccountID(sfAccount, account);
if (feeVal != 0)
newEntry.setFieldU16(sfTradingFee, feeVal);
newEntry.setFieldU32(
sfVoteWeight,
static_cast<std::int64_t>(Number(lpTokens) * VOTE_WEIGHT_SCALE_FACTOR / lptAMMBalance));
// Find an entry with the least tokens/fee. Make the order deterministic
// if the tokens/fees are equal.
if (!minTokens ||
(lpTokens < *minTokens ||
(lpTokens == *minTokens &&
(feeVal < minFee || (feeVal == minFee && account < minAccount)))))
{
minTokens = lpTokens;
minPos = updatedVoteSlots.size();
minAccount = account;
minFee = feeVal;
}
updatedVoteSlots.push_back(std::move(newEntry));
}
// The account doesn't have the vote entry.
if (!foundAccount)
{
auto update = [&](std::optional<std::uint8_t> const& minPos = std::nullopt) {
STObject newEntry = STObject::makeInnerObject(sfVoteEntry);
if (feeNew != 0)
newEntry.setFieldU16(sfTradingFee, feeNew);
newEntry.setFieldU32(
sfVoteWeight,
static_cast<std::int64_t>(
Number(lpTokensNew) * VOTE_WEIGHT_SCALE_FACTOR / lptAMMBalance));
newEntry.setAccountID(sfAccount, account_);
num += feeNew * lpTokensNew;
den += lpTokensNew;
if (minPos)
*(updatedVoteSlots.begin() + *minPos) = std::move(newEntry);
else
updatedVoteSlots.push_back(std::move(newEntry));
};
// Add new entry if the number of the vote entries
// is less than Max.
if (updatedVoteSlots.size() < VOTE_MAX_SLOTS)
update();
// Add the entry if the account has more tokens than
// the least token holder or same tokens and higher fee.
else if (lpTokensNew > *minTokens || (lpTokensNew == *minTokens && feeNew > minFee))
{
auto const entry = updatedVoteSlots.begin() + minPos;
// Remove the least token vote entry.
num -= Number((*entry)[~sfTradingFee].value_or(0)) * *minTokens;
den -= *minTokens;
update(minPos);
}
// All slots are full and the account does not hold more LPTokens.
// Update anyway to refresh the slots.
else
{
JLOG(j_.debug()) << "AMMVote::applyVote, insufficient tokens to "
"override other votes";
}
}
XRPL_ASSERT(
!ctx_.view().rules().enabled(fixInnerObjTemplate) || ammSle->isFieldPresent(sfAuctionSlot),
"xrpl::applyVote : has auction slot");
// Update the vote entries and the trading/discounted fee.
ammSle->setFieldArray(sfVoteSlots, updatedVoteSlots);
if (auto const fee = static_cast<std::int64_t>(num / den))
{
ammSle->setFieldU16(sfTradingFee, fee);
if (ammSle->isFieldPresent(sfAuctionSlot))
{
auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
if (auto const discountedFee = fee / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION)
auctionSlot.setFieldU16(sfDiscountedFee, discountedFee);
else if (auctionSlot.isFieldPresent(sfDiscountedFee))
auctionSlot.makeFieldAbsent(sfDiscountedFee);
}
}
else
{
if (ammSle->isFieldPresent(sfTradingFee))
ammSle->makeFieldAbsent(sfTradingFee);
if (ammSle->isFieldPresent(sfAuctionSlot))
{
auto& auctionSlot = ammSle->peekFieldObject(sfAuctionSlot);
if (auctionSlot.isFieldPresent(sfDiscountedFee))
auctionSlot.makeFieldAbsent(sfDiscountedFee);
}
}
sb.update(ammSle);
return {tesSUCCESS, true};
}
TER
AMMVote::doApply()
{
// This is the ledger view that we work against. Transactions are applied
// as we go on processing transactions.
Sandbox sb(&ctx_.view());
auto const result = applyVote(ctx_, sb, account_, j_);
if (result.second)
sb.apply(ctx_.rawView());
return result.first;
}
} // namespace xrpl