From 96dbb504fa5aec02b0120fc9b0fc56d25b581374 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Thu, 7 May 2026 13:44:18 -0400 Subject: [PATCH] feat: add fastswap token tier system and fix orphan surplus_eth --- tools/fastswap-miles/miles.go | 6 +- tools/fastswap-miles/tiers.go | 112 ++++++++++++++++++++++++++++ tools/fastswap-miles/tiers_test.go | 114 +++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 tools/fastswap-miles/tiers.go create mode 100644 tools/fastswap-miles/tiers_test.go diff --git a/tools/fastswap-miles/miles.go b/tools/fastswap-miles/miles.go index d63be0af1..b2a872190 100644 --- a/tools/fastswap-miles/miles.go +++ b/tools/fastswap-miles/miles.go @@ -394,9 +394,11 @@ WHERE processed = false cfg.Logger.Info("erc20 tx not in FastRPC after retry window, skipping with 0 miles", slog.String("tx", r.txHash), slog.String("user", r.user), slog.Duration("age", txAge)) - surplusWei, _ := new(big.Int).SetString(r.surplus, 10) if !cfg.DryRun { - markProcessed(cfg.DB, r.txHash, weiToEth(surplusWei), 0, 0, "0") + // surplus_eth=0 because surplus is in raw output-token units; + // converting it via weiToEth (as the prior code did) yields a + // nonsense value for non-ETH outputs (e.g. PEPE 14,413 ETH). + markProcessed(cfg.DB, r.txHash, 0, 0, 0, "0") } processed++ continue diff --git a/tools/fastswap-miles/tiers.go b/tools/fastswap-miles/tiers.go new file mode 100644 index 000000000..e893d0da2 --- /dev/null +++ b/tools/fastswap-miles/tiers.go @@ -0,0 +1,112 @@ +package main + +import ( + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +// Tier classifies a token by price-stability profile. The tier drives sweep +// cadence, the percentile of recent sweep gas used to estimate per-user cost +// at miles-awarding time, and the gas cap above which sweeps are deferred. +type Tier int + +const ( + TierStable Tier = iota + TierBlueChip + TierVolatile +) + +func (t Tier) String() string { + switch t { + case TierStable: + return "stable" + case TierBlueChip: + return "bluechip" + case TierVolatile: + return "volatile" + default: + return "unknown" + } +} + +// tokenConfig holds the per-token sweep parameters. Values are tuned by tier +// and refined per token where the realized data justifies a different default. +type tokenConfig struct { + Tier Tier + SweepCadence time.Duration // target maximum interval between sweeps + CostEstimatePctile int // percentile of recent sweep gas to use as upfront cost estimate + ExpectedBatchSize int // assumed batch size for per-user cost dilution +} + +// L1 mainnet token addresses for the configured set. +var ( + usdcAddr = common.HexToAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") + usdtAddr = common.HexToAddress("0xdac17f958d2ee523a2206206994597c13d831ec7") + daiAddr = common.HexToAddress("0x6b175474e89094c44da98b954eedeac495271d0f") + wbtcAddr = common.HexToAddress("0x2260fac5e5542a773aa44fbcfedf7c193bc2c599") + arbAddr = common.HexToAddress("0xb50721bcf8d664c30412cfbc6cf7a15145234ad1") + linkAddr = common.HexToAddress("0x514910771af9ca656af840dff83e8264ecf986ca") + compAddr = common.HexToAddress("0xc00e94cb662c3520282e6f5717214004a7f26888") + uniAddr = common.HexToAddress("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984") + sushiAddr = common.HexToAddress("0x6b3595068778dd592e39a122f4f5a5cf09c90fe2") + inchAddr = common.HexToAddress("0x111111111117dc0aa78b770fa6a738034120c302") + yfiAddr = common.HexToAddress("0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9") + pepeAddr = common.HexToAddress("0x6982508145454ce325ddbe47a25d4ec3d2311933") +) + +// tokenConfigs maps known L1 token addresses (lowercased hex) to their sweep +// configuration. Lookup is case-insensitive via lookupTokenConfig. Unknown +// tokens fall through to defaultTokenConfig. +// +// Cadence values are derived from the Apr 13-27 stable-volume window: +// - USDC (~31 swaps/day): daily yields ~30-row batches. +// - USDT (~17 swaps/day): every 2d yields ~34-row batches. +// - DAI (~8 swaps/day, mostly 1-3/day): every 5d yields ~38-row batches. +// - Blue chips (3-15 swaps/day): daily is reasonable; batch sizes are smaller. +// - Volatile (low/sporadic volume): 6h to limit price-risk exposure. +// +// CostEstimatePctile and ExpectedBatchSize together set how much "buffer" the +// protocol keeps when paying out miles upfront. Stables use a low percentile +// (p40) and a generous batch-size assumption (30) because their realized cost +// is consistent and batches reliably exceed 30; the difference is protocol +// upside. Volatiles use p75 and batch-size 1 — the worst-case assumption. +var tokenConfigs = map[common.Address]tokenConfig{ + usdcAddr: {TierStable, 24 * time.Hour, 40, 30}, + usdtAddr: {TierStable, 48 * time.Hour, 40, 30}, + daiAddr: {TierStable, 120 * time.Hour, 40, 30}, + wbtcAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + arbAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + linkAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + compAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + uniAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + sushiAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + inchAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + yfiAddr: {TierBlueChip, 24 * time.Hour, 50, 3}, + pepeAddr: {TierVolatile, 6 * time.Hour, 75, 1}, +} + +// defaultTokenConfig is used when an output token is not in tokenConfigs. +// Conservative defaults: treat as volatile, sweep every 6h, assume size-1 +// batches at p75 of recent costs. +var defaultTokenConfig = tokenConfig{ + Tier: TierVolatile, + SweepCadence: 6 * time.Hour, + CostEstimatePctile: 75, + ExpectedBatchSize: 1, +} + +// lookupTokenConfig returns the configuration for a token address, case +// insensitive. Unknown addresses get defaultTokenConfig. +func lookupTokenConfig(addr common.Address) tokenConfig { + if cfg, ok := tokenConfigs[addr]; ok { + return cfg + } + // tokenConfigs is keyed by checksum address from common.HexToAddress; the + // caller may pass a lowercased value. Normalize to be safe. + if cfg, ok := tokenConfigs[common.HexToAddress(strings.ToLower(addr.Hex()))]; ok { + return cfg + } + return defaultTokenConfig +} diff --git a/tools/fastswap-miles/tiers_test.go b/tools/fastswap-miles/tiers_test.go new file mode 100644 index 000000000..e1fb8213a --- /dev/null +++ b/tools/fastswap-miles/tiers_test.go @@ -0,0 +1,114 @@ +package main + +import ( + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +func TestTierString(t *testing.T) { + cases := []struct { + tier Tier + want string + }{ + {TierStable, "stable"}, + {TierBlueChip, "bluechip"}, + {TierVolatile, "volatile"}, + {Tier(99), "unknown"}, + } + for _, c := range cases { + if got := c.tier.String(); got != c.want { + t.Errorf("Tier(%d).String() = %q, want %q", c.tier, got, c.want) + } + } +} + +func TestLookupTokenConfig_KnownStable(t *testing.T) { + cfg := lookupTokenConfig(usdcAddr) + if cfg.Tier != TierStable { + t.Errorf("USDC tier = %v, want stable", cfg.Tier) + } + if cfg.SweepCadence != 24*time.Hour { + t.Errorf("USDC cadence = %v, want 24h", cfg.SweepCadence) + } + if cfg.CostEstimatePctile != 40 { + t.Errorf("USDC pctile = %d, want 40", cfg.CostEstimatePctile) + } + if cfg.ExpectedBatchSize != 30 { + t.Errorf("USDC batch = %d, want 30", cfg.ExpectedBatchSize) + } +} + +func TestLookupTokenConfig_KnownBlueChip(t *testing.T) { + cfg := lookupTokenConfig(wbtcAddr) + if cfg.Tier != TierBlueChip { + t.Errorf("WBTC tier = %v, want bluechip", cfg.Tier) + } + if cfg.ExpectedBatchSize != 3 { + t.Errorf("WBTC batch = %d, want 3", cfg.ExpectedBatchSize) + } +} + +func TestLookupTokenConfig_KnownVolatile(t *testing.T) { + cfg := lookupTokenConfig(pepeAddr) + if cfg.Tier != TierVolatile { + t.Errorf("PEPE tier = %v, want volatile", cfg.Tier) + } + if cfg.SweepCadence != 6*time.Hour { + t.Errorf("PEPE cadence = %v, want 6h", cfg.SweepCadence) + } +} + +func TestLookupTokenConfig_UnknownDefaultsVolatile(t *testing.T) { + addr := common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + cfg := lookupTokenConfig(addr) + if cfg != defaultTokenConfig { + t.Errorf("unknown addr config = %+v, want defaultTokenConfig %+v", cfg, defaultTokenConfig) + } + if cfg.Tier != TierVolatile { + t.Errorf("default tier = %v, want volatile", cfg.Tier) + } +} + +func TestLookupTokenConfig_CaseInsensitive(t *testing.T) { + // Same address, different cases. + upper := common.HexToAddress(strings.ToUpper(usdcAddr.Hex())) + lower := common.HexToAddress(strings.ToLower(usdcAddr.Hex())) + if lookupTokenConfig(upper).Tier != TierStable { + t.Errorf("upper-case USDC not recognized") + } + if lookupTokenConfig(lower).Tier != TierStable { + t.Errorf("lower-case USDC not recognized") + } +} + +func TestAllStableConfigsConsistent(t *testing.T) { + stables := []common.Address{usdcAddr, usdtAddr, daiAddr} + for _, a := range stables { + cfg := lookupTokenConfig(a) + if cfg.Tier != TierStable { + t.Errorf("%s tier = %v, want stable", a.Hex(), cfg.Tier) + } + if cfg.CostEstimatePctile != 40 { + t.Errorf("%s pctile = %d, want 40", a.Hex(), cfg.CostEstimatePctile) + } + if cfg.ExpectedBatchSize != 30 { + t.Errorf("%s batch = %d, want 30", a.Hex(), cfg.ExpectedBatchSize) + } + } +} + +func TestAllBlueChipConfigsConsistent(t *testing.T) { + blueChips := []common.Address{wbtcAddr, arbAddr, linkAddr, compAddr, uniAddr, sushiAddr, inchAddr, yfiAddr} + for _, a := range blueChips { + cfg := lookupTokenConfig(a) + if cfg.Tier != TierBlueChip { + t.Errorf("%s tier = %v, want bluechip", a.Hex(), cfg.Tier) + } + if cfg.SweepCadence != 24*time.Hour { + t.Errorf("%s cadence = %v, want 24h", a.Hex(), cfg.SweepCadence) + } + } +}