Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions tools/fastswap-miles/miles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
112 changes: 112 additions & 0 deletions tools/fastswap-miles/tiers.go
Original file line number Diff line number Diff line change
@@ -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
}
114 changes: 114 additions & 0 deletions tools/fastswap-miles/tiers_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading