93: Add a minimum EVE burn amount and retained reserve to the Evelyn IOU burner

This commit is contained in:
2026-10-06 08:27:59 +02:00
parent 49df1ac9d0
commit 00a7f13c6f
11 changed files with 394 additions and 26 deletions
@@ -23,6 +23,11 @@ import java.util.List;
* internally through the Currency Identity Service and Solana and are not
* part of Evelyn's input configuration.</p>
*
* <p>Two independent EVE-denominated {@link MoneyAmount} values configure the
* minimum per burn and retained EVE reserve. Their canonical EVE UUID is
* authoritative; the minimum must be strictly positive and the reserve
* non-negative. A burn threshold does not suppress independent buybacks.</p>
*
* <p>Every non-overlapping iteration has this order:</p>
* <ol>
* <li>If a prior burn has a pending signature, await only that exact
@@ -56,11 +61,16 @@ import java.util.List;
* <li>Reload the complete visible Evelyn IOU balance, including tokens
* deposited or acquired before this iteration. Absence or failure of a
* swap does not suppress this step, and the input buyback limit never caps
* the amount eligible for burning.</li>
* the amount eligible for burning. Subtract the retained EVE reserve and
* round the positive remainder down to EVE mint precision. If the actual
* rounded amount is zero or below the burn minimum, keep the EVE for a
* later iteration and send no burn notification. Equality qualifies;</n+ * never round upward or consume reserve to reach the minimum.</li>
* <li>Require a positive, sufficiently fresh canonical EVE/USDT price and
* calculate the complete balance's dollar value before submitting a burn.
* calculate the actual eligible burn amount's dollar value, excluding
* the retained reserve, before submitting a burn.
* Missing, invalid, or stale price data postpones the burn.</li>
* <li>Submit the complete EVE balance once and retain its signature,
* <li>Submit all eligible rounded EVE above reserve once, without a burn
* maximum or fixed batch size, and retain its signature,
* amount, and notification value as pending before waiting. The service
* never automatically retries, rebuilds, re-signs, or resubmits a swap or
* burn because timeout, interruption, or communication failure can leave
@@ -85,8 +85,16 @@ public final class EvelynIOUBurnerServiceImpl
* equality qualifies without rounding up or spending the reserve. Three
* empty input lists permit direct-EVE-only burning.</p>
*
* <p>{@code minimumBurnAmount} and {@code eveReserve} must identify EVE by
* its canonical UUID, regardless of supplied name/symbol. The minimum must
* be strictly positive and the reserve non-negative. A new burn uses the
* entire balance above reserve, rounded down to EVE mint precision, only
* when that actual amount is positive and at least the minimum. Equality
* qualifies; fractional remainders stay in the wallet. This policy does
* not suppress independent small buybacks or re-evaluate pending burns.</p>
*
* @param burnerWallet dedicated wallet whose balances are inspected and
* whose complete EVE balance is burned
* whose eligible EVE above reserve is burned
* @param swapService exact-input Jupiter service; normal return already
* means the swap reached {@code CONFIRMED}
* @param solanaBlockChain Solana access used for mint ownership, token
@@ -108,6 +116,10 @@ public final class EvelynIOUBurnerServiceImpl
* @param inputCurrencyMaximumSwapAmounts exactly one strictly positive
* token-denominated buyback maximum per accepted input UUID;
* it does not limit burns
* @param minimumBurnAmount strictly positive EVE-denominated minimum for
* the actual rounded amount of each new burn
* @param eveReserve non-negative EVE-denominated amount retained after a
* successful burn; zero is valid
* @param maximumSlippageBps Jupiter slippage limit in basis points from 0
* through 10000 inclusive
* @param maximumPriceAge strictly positive maximum observation age in
@@ -126,7 +138,9 @@ public final class EvelynIOUBurnerServiceImpl
* lacks exactly one usable Solana mint,
* or minimum/maximum UUIDs do not match inputs,
* a swap bound is non-positive, or a
* minimum exceeds its maximum
* minimum exceeds its maximum, or the
* burn minimum/reserve does not identify
* EVE or has an invalid numeric value
*/
public EvelynIOUBurnerServiceImpl(
@NotNull SolanaWallet burnerWallet,
@@ -138,6 +152,8 @@ public final class EvelynIOUBurnerServiceImpl
@NotNull List<MoneyAmount> inputCurrencyMinimumReserves,
@NotNull List<MoneyAmount> inputCurrencyMinimumSwapAmounts,
@NotNull List<MoneyAmount> inputCurrencyMaximumSwapAmounts,
@NotNull MoneyAmount minimumBurnAmount,
@NotNull MoneyAmount eveReserve,
int maximumSlippageBps,
ΩmilliSecondsΩ maximumPriceAge,
@NotNull Duration burnFinalizationTimeout,
@@ -211,6 +227,12 @@ public final class EvelynIOUBurnerServiceImpl
USDT_ID,
"USDT"
);
this.minimumBurnAmount = validateEveAmount(
minimumBurnAmount, "minimumBurnAmount", canonicalEve, true
);
this.eveReserve = validateEveAmount(
eveReserve, "eveReserve", canonicalEve, false
);
this.eveUsdtTradingPair = new TradingPair(
canonicalEve,
canonicalUsdt
@@ -519,9 +541,9 @@ public final class EvelynIOUBurnerServiceImpl
private void processCompleteEveBalance() throws InterruptedException {
ΩAmountΩ eveBalance;
SolanaSPLTokenProgram eveTokenProgram;
try {
SolanaSPLTokenProgram eveTokenProgram =
resolveSupportedTokenProgram(EVELYN_IOU_MINT);
eveTokenProgram = resolveSupportedTokenProgram(EVELYN_IOU_MINT);
eveBalance = burnerWallet.getSPLTokenBalance(
EVELYN_IOU_MINT,
eveTokenProgram
@@ -554,17 +576,40 @@ public final class EvelynIOUBurnerServiceImpl
return;
}
ΩAmountΩ availableForBurn = eveBalance.subtract(eveReserve.amount());
if (availableForBurn.signum() <= 0) {
return;
}
ΩAmountΩ amountToBurn;
try {
ΩamountDecimalsΩ decimals = resolveMintDecimals(
EVELYN_IOU_MINT, eveTokenProgram
);
amountToBurn = availableForBurn.setScale(decimals, RoundingMode.DOWN);
} catch (IOException | RuntimeException exception) {
log.warn(
"Could not resolve Evelyn IOU mint precision; burn is "
+ "postponed: failureType={}",
exception.getClass().getName()
);
return;
}
if (amountToBurn.signum() <= 0
|| amountToBurn.compareTo(minimumBurnAmount.amount()) < 0) {
return;
}
ΩPriceΩ evePriceInUsdt = findFreshPrice(eveUsdtTradingPair);
if (evePriceInUsdt == null) {
return;
}
ΩAmountΩ dollarValue = eveBalance.multiply(evePriceInUsdt);
ΩAmountΩ dollarValue = amountToBurn.multiply(evePriceInUsdt);
ΩSolanaTransactionSignatureΩ burnSignature;
try {
burnSignature = burnerWallet.burnSPLToken(
EVELYN_IOU_MINT,
eveBalance
amountToBurn
);
} catch (IOException exception) {
burnsSuspended = true;
@@ -607,7 +652,7 @@ public final class EvelynIOUBurnerServiceImpl
pendingBurn = new PendingBurn(
burnSignature,
eveBalance,
amountToBurn,
dollarValue
);
awaitPendingBurn();
@@ -826,6 +871,31 @@ public final class EvelynIOUBurnerServiceImpl
return supply.decimals();
}
private static MoneyAmount validateEveAmount(
MoneyAmount configuredAmount,
String configurationName,
CurrencyType canonicalEve,
boolean strictlyPositive
) {
Objects.requireNonNull(configuredAmount, configurationName);
Objects.requireNonNull(configuredAmount.amount(), configurationName + ".amount");
CurrencyType currency = Objects.requireNonNull(
configuredAmount.currencyType(), configurationName + ".currencyType"
);
UUID currencyId = Objects.requireNonNull(
currency.id(), configurationName + ".currencyType.id"
);
if (!EVE_ID.equals(currencyId)) {
throw new IllegalArgumentException(configurationName + " must identify EVE by UUID");
}
if (configuredAmount.amount().signum() < 0
|| (strictlyPositive && configuredAmount.amount().signum() == 0)) {
throw new IllegalArgumentException(configurationName
+ (strictlyPositive ? " must be greater than zero" : " must be zero or greater"));
}
return new MoneyAmount(configuredAmount.amount(), canonicalEve);
}
private List<MoneyAmount> validateAndCopyInputs(
List<MoneyAmount> configuredInputs
) {
@@ -1059,6 +1129,8 @@ public final class EvelynIOUBurnerServiceImpl
private final List<MoneyAmount> inputCurrencyMinimumReserves;
private final Map<UUID, MoneyAmount> inputCurrencyMinimumSwapAmounts;
private final Map<UUID, MoneyAmount> inputCurrencyMaximumSwapAmounts;
private final MoneyAmount minimumBurnAmount;
private final MoneyAmount eveReserve;
private final int maximumSlippageBps;
private final ΩmilliSecondsΩ maximumPriceAge;
private final Duration maximumPriceAgeDuration;
@@ -329,6 +329,8 @@ public final class NenjimHubImpl implements NenjimHub {
inputCurrencyMinimumReserves,
inputCurrencyMinimumSwapAmounts,
inputCurrencyMaximumSwapAmounts,
new MoneyAmount(new ΩAmountΩ("0.1"), cis.resolve(CurrencyTypeIds.EVE_ID)),
new MoneyAmount(new ΩAmountΩ("0.01"), cis.resolve(CurrencyTypeIds.EVE_ID)),
200,
2*1000*60L,
Duration.ofSeconds(10),