fix(amm): address Copilot review — exact BigInt min_out, fail on oversized pool fee, sync flake run docs

This commit is contained in:
Andrea Franz 2026-07-23 13:51:26 +02:00 committed by r4bbit
parent 9b7a3dcaac
commit fe41baf210
4 changed files with 53 additions and 4 deletions

View File

@ -18,8 +18,10 @@
# fails flake evaluation because the app directory is copied into the Nix
# store as its own flake root, so `../..` can't escape it there. Instead,
# the repo-root flake.nix builds this module directly (src = ./apps/amm)
# and resolves amm_client_ffi via `self`. Build/run the app from the repo
# root, e.g. `nix build .#packages.<system>.default` or `nix run .`.
# and resolves amm_client_ffi via `self`. The repo-root flake exposes the UI
# as a named attribute (there is no bare `default`): run it from the repo root
# with `nix run .#amm-ui`, and build just the FFI crate with
# `nix build .#amm_client_ffi`.
outputs = inputs@{ logos-module-builder, ... }:
logos-module-builder.lib.mkLogosQmlModule {
src = ./.;

View File

@ -239,7 +239,15 @@ Rectangle {
root.swapInProgress = true
root.swapError = ""
var minOutStr = root.formatBaseUnits(root.minReceivedAmount)
// Compute the submitted slippage floor with exact integer (BigInt) math
// rather than the double-based preview: base-unit values for 18-decimal
// tokens exceed 2^53, where doubles would understate min_out and weaken
// price protection. Sell/buy reserves follow the pool's canonical order.
var minOutStr = swapState.minOutBaseUnits(
root.sellInput,
root.sellIsPoolA ? root.poolReserveA : root.poolReserveB,
root.sellIsPoolA ? root.poolReserveB : root.poolReserveA,
root.slippageTolerancePercent)
// Max u64 sentinel: "ignore deadline", per AmmUiBackend.rep.
var deadline = "18446744073709551615"

View File

@ -73,6 +73,44 @@ QtObject {
return safeAmount * (1 - safeSlippage / 100);
}
// Exact-integer minimum-received (base units) for a SwapExactInput, used as
// the on-chain slippage floor that is actually submitted. Computed in BigInt
// (arbitrary precision, mirroring the on-chain u256 math): base units for
// 18-decimal tokens exceed 2^53 and even overflow u128 intermediates, so JS
// doubles silently lose precision and would understate min_out weakening
// the user's price protection. amountIn/reserveIn/reserveOut are base-unit
// integer strings; returns a decimal string. Falls back to the double
// estimate only if BigInt is unavailable in this Qt build.
function minOutBaseUnits(amountIn, reserveIn, reserveOut, slippagePercent) {
if (typeof BigInt !== "undefined") {
var toBig = function (x) {
var s = String(x).trim();
return /^[0-9]+$/.test(s) ? BigInt(s) : BigInt(0);
};
var zero = BigInt(0);
var denom = BigInt(10000);
var amtIn = toBig(amountIn);
var resIn = toBig(reserveIn);
var resOut = toBig(reserveOut);
if (amtIn <= zero || resIn <= zero || resOut <= zero)
return "0";
var feeBps = BigInt(Math.round(Math.min(10000, Math.max(0, Number(root.feeBps) || 0))));
var amtInAfterFee = amtIn * (denom - feeBps) / denom; // floor
if (amtInAfterFee <= zero)
return "0";
var out = resOut * amtInAfterFee / (resIn + amtInAfterFee); // floor
var slipBps = BigInt(Math.round(clampSlippagePercent(slippagePercent) * 100));
if (slipBps < zero) slipBps = zero;
if (slipBps > denom) slipBps = denom;
var minOut = out * (denom - slipBps) / denom; // floor
return minOut.toString();
}
// Legacy double fallback (no worse than before if BigInt is missing).
var estOut = amountOutFor(amountIn, reserveIn, reserveOut);
return String(Math.floor(Math.max(0, minReceived(estOut, slippagePercent))));
}
function maxSent(amountIn, slippagePercent) {
const safeAmount = parseAmount(amountIn);
const safeSlippage = clampSlippagePercent(slippagePercent);

View File

@ -27,7 +27,8 @@ pub fn decode_pool(bytes: &[u8]) -> Result<PoolView, String> {
reserve_a: p.reserve_a,
reserve_b: p.reserve_b,
liquidity_supply: p.liquidity_pool_supply,
fees: u32::try_from(p.fees).unwrap_or(u32::MAX),
fees: u32::try_from(p.fees)
.map_err(|_| format!("pool fee {} exceeds u32::MAX", p.fees))?,
})
}