whitepaper · v0.1 · draft
DualCurve: a two-threshold reward protocol for coordinated holding
Technical specification, security model, and known limitations.
version
0.1 (draft)
target chain
Base mainnet
token
DCRV · 1B fixed supply
Abstract
We describe DualCurve, an experimental protocol for redistributing trading-fee revenue back to token holders through two distinct reward pools running on different timescales. The protocol applies a 5% hook fee on every swap routed through the official DCRV/ETH Uniswap v4 pool — taken on both buys and sells. Collected ETH is split 75 / 25 between a SmallCurveDistributor — which fires automatically each time it accumulates 5 ETH and pushes the proceeds pro-rata to the top 100 eligible holders — and a LargeCurveAccumulator — which collects up to 500 ETH before opening a merkle-based claim window for the top 20 holders.
The design separates the ERC-20 contract from all reward logic: the token itself is a clean, composable ERC-20 with no transfer hooks, while tax enforcement lives exclusively inside the v4 hook. Holder ranking is maintained on-chain via a bounded Solady min-heap with no privileged operator. The protocol is designed to be deployed and renounced; once initialised there is no upgrade path.
Motivation
Standard fee-on-transfer tokens collect taxes inside _transfer, applying a deduction to every token movement. While operationally simple, this has well-documented drawbacks: composability breaks with most DeFi primitives (lending vaults, bridges, AMMs without tax-aware integrations), and the tax fires on flows that have no semantic relationship to trading — wallet rebalancing, multisig operations, contract-to-contract calls.
Holder-reward mechanisms in existing tokens tend to suffer from one or more of:
- ▸linear distribution to all holders, including stale wallets and dust accounts, which dilutes returns to active participants
- ▸push distribution with unbounded recipient lists, opening DoS via malicious smart-contract recipients
- ▸mutable parameters controlled by privileged roles, creating regulatory and trust overhead
- ▸a single timescale — either frequent micro-payouts or one jackpot event, but not both
DualCurve addresses these by (i) isolating tax logic to a single venue (the official Uniswap v4 pool), keeping the ERC-20 minimal and composable; (ii) bounding rewards to a small number of top holders, making distribution accounting tractable; (iii) combining two timescales — frequent automatic distribution and rare claim-based jackpot — in a single coordinated system; and (iv) renouncing ownership after configuration so that no party can modify parameters post-launch.
Protocol architecture
The system is partitioned into seven contracts with strictly scoped responsibilities. No contract holds knowledge it does not need; failure in any single component does not cascade.
┌─────────────────────┐
│ DualCurveToken │ clean ERC-20 + forwards
│ (1B fixed supply) │ _update → HolderRanking
└──────────┬──────────┘
│
▼
┌─────────────────────┐ on _update
│ HolderRanking │ ◀─────────── ranking heap (top-100, Solady MinHeap)
└─────────────────────┘
▲
│ live read
│
┌─────────────────────┐
│ SnapshotManager │ anti-frontrun lookback gate
└─────────────────────┘
▲
│ requestSnapshot / executeSnapshot
│
user swap ─▶ Uniswap v4 Pool ─▶ DualCurveHook ─▶ FeeRouter
(5% buy+sell) │
│ 75 / 25
▼
┌───────────────┐ ┌─────────────────┐
│ SmallCurve │ │ LargeCurve │
│ Distributor │ │ Accumulator │
│ push, 5 ETH, │ │ pull, 500 ETH, │
│ top 100 │ │ top 20, merkle, │
└───────────────┘ │ 30d window │
└─────────────────┘DualCurveToken.sol
The token is intentionally minimal. It extends OpenZeppelin ERC20 and Ownable, mints a fixed supply of 1,000,000,000 DCRV to the deployer at construction, and exposes one additional function: setHolderRanking, which wires the token to the on-chain ranking module exactly once.
The only override on _update is a single forwarding call to HolderRanking.onBalanceChange after the standard ERC-20 update completes. There is no tax inside the token — the protocol fee lives exclusively in the v4 hook. The token remains fully composable with the rest of DeFi.
DualCurveHook.sol (Uniswap v4)
The hook is the only place where DCRV-specific tax logic exists. It implements beforeSwap and afterSwap against the Uniswap v4 PoolManager and applies the tax exclusively to swaps routed through the official pool.
- ▸afterSwap skims 5% of the output ETH on sells (DCRV → ETH) using AFTER_SWAP_RETURNS_DELTA.
- ▸beforeSwap skims 5% of the input ETH on buys (ETH → DCRV) via BeforeSwapDelta(deltaSpecified > 0) + poolManager.take.
- ▸the hook address must encode the four permission bits BEFORE_SWAP | BEFORE_SWAP_RETURNS_DELTA | AFTER_SWAP | AFTER_SWAP_RETURNS_DELTA (0xCC), mined via CREATE2 salt search.
v4-specific note
Uniswap v4 uses delta accounting rather than direct ERC-20 transfers during callbacks. The hook does not move tokens via transfer; it settles deltas with the PoolManager via poolManager.take for ETH skimmed from each leg.
FeeRouter.sol
The router exists to keep splitting logic out of the hook. It receives ETH via routeFees() (and a fallback receive()), divides the incoming amount by the configured ratio (default 7500 / 2500), and forwards to the two curve contracts. Integer-division dust always goes to LargeCurve so that SmallCurve’s accounting (which has its own dust handling) is not disturbed.
SmallCurveDistributor.sol
The small curve accumulates ETH up to a threshold of 5 ETH. When deposit causes the running balance to cross the threshold, the contract requests a snapshot from SnapshotManager with a 10-block lookback. After the lookback elapses, anyone can call executeDistribution which reads the snapshot (top 100 holders, hold-time filtered), computes rewardi = pool × (balancei / sum_balances), and pushes ETH to each recipient with call{value: r, gas: 30_000}.
Failed pushes (malicious receiver contracts, gas grief, etc.) do not block the loop: the unsent share lands in the reclaimable mapping and the user can pull it later via claimReclaimable. The gas limit on the push transfer is what keeps the loop bounded.
LargeCurveAccumulator.sol
The large curve uses a fundamentally different distribution model. Push distribution at 500 ETH thresholds is operationally fragile (any failure mode is consequential), so we use a pull-based merkle-claim pattern:
- ▸ETH accumulates via deposit() from the FeeRouter.
- ▸When the accumulated balance reaches 500 ETH, anyone can call requestDistribution which arms a 100-block lookback.
- ▸After the lookback, anyone can call executeDistribution; the contract reads the top-20 snapshot, computes per-holder shares, builds the merkle tree on-chain using OpenZeppelin Hashes.commutativeKeccak256, and stores the root + total + 30-day deadline in a new Round.
- ▸Eligible winners call claim(roundId, amount, proof) with a merkle proof of inclusion.
- ▸Claims remain open for 30 days; unclaimed funds are swept by anyone via sweepExpired(roundId) which returns the unclaimed ETH to SmallCurve.receiveSurplus(), re-feeding the small curve.
merkle tree on-chain
Unlike many merkle-airdrop designs, DualCurve builds the tree on-chain from the snapshot, not from a root submitted off-chain. There is no operator and no dispute window — the root is a deterministic function of the snapshot, which is itself derived from on-chain balances.
HolderRanking.sol
The ranking is maintained on-chain as a bounded min-heap of size 100 (using Solady MinHeapLib) keyed by encoded balance. Each entry packs the balance in the upper 96 bits and the address in the lower 160 bits, so the heap root is always the lowest balance in the top 100, enabling O(log N) insertion and O(1) threshold comparison.
On every token transfer the token calls HolderRanking.onBalanceChange. When an existing top-100 holder’s balance changes the heap array is rebuilt with the updated entry (O(N log N), N=100). Entries dropping below 0.1% of supply leave the heap; new entries enter only if they exceed the current minimum.
SnapshotManager.sol
The snapshot manager is the anti-frontrun gate. Both curve contracts request snapshots through it. A request stores the earliest block at which it can be executed (current block + lookback). When executed it reads the live ranking, intersects with hold-time-eligible holders, and stores an immutable snapshot record.
Critically: the snapshot used by an executing distribution is the one materialised at execute time, not at request time. The lookback enforces that any attempt to manipulate the ranking right before the distribution is visible on-chain for 10 / 100 blocks (SmallCurve / LargeCurve respectively) and can be front-run by an honest executor.
Reward mechanics
Distribution formula
For both curves, the reward to recipient i in a given round is:
rewardi = P · ( bi / Σ bj )
where P = pool size at execute time, bi = balance of recipient i at the snapshot block, and the sum runs over eligible recipients (top 100 for SmallCurve, top 20 for LargeCurve). The formula is identical between curves; only the recipient set differs.
This is proportional, not flat. A holder with twice the balance of another receives twice the reward. Flat distribution would incentivise Sybil fragmentation; proportional distribution makes fragmentation neutral at best and costly at worst (gas overhead).
Cadence analysis
Round frequency is volume-dependent. The expected time-to-fire is:
Tfire = threshold / ( Vswap · 0.05 · curve_share )
where Vswap is daily swap volume in ETH terms.
| daily volume | small fires every | large fires every |
|---|---|---|
| $50,000 (~20 ETH) | ~6.7 hours | ~333 days |
| $250,000 (~100 ETH) | ~1.3 hours | ~67 days |
| $1,000,000 (~400 ETH) | ~20 minutes | ~17 days |
| $5,000,000 (~2,000 ETH) | ~4 minutes | ~3.3 days |
The asymmetry is intentional. At realistic mid-cap volumes the small curve provides hourly-to-multi-hourly feedback while the large curve remains a multi-week event. The two timescales coexist without one dominating the other.
Anti-Sybil & anti-MEV
Sybil resistance
The naive attack against a top-N ranking is fragmentation: split a large holding across many wallets to occupy multiple positions simultaneously. DualCurve deploys three independent defenses:
- ▸Minimum holding threshold (0.1% supply). A wallet must hold at least 1,000,000 DCRV (0.1% of 1B supply) to be considered for the ranking at all. Fragmenting 100M DCRV (10% supply) into 100 wallets of 1M each is the cleanest path, but every fragment now competes for one slot against the consolidated position the original wallet would have occupied.
- ▸Minimum holding time (~1 hour). A wallet must hold its tokens for at least 1,800 blocks on Base (≈ 1 hour) before becoming eligible. Same-block fragmentation right before a snapshot does not qualify; the attacker must hold patiently.
- ▸Gas-imposed friction. The on-chain heap rebuilds at every state change touching the top-100. Each fragmentation transfer pays gas and triggers a heap re-fold. The combination with the 0.1% gate makes fragmentation require meaningful capital with no proportional benefit.
These defenses do not eliminate Sybil under arbitrary attacker budget — they raise the cost above the expected reward. A formal attacker break-even is given in §6.
MEV resistance
The primary MEV concern is front-running the trigger: an attacker observes the small curve approaching threshold and (a) buys a large position immediately before the trigger to be included in the snapshot, or (b) uses a flash loan to temporarily inflate balance during the snapshot block.
Both are blocked by the retroactive snapshot mechanism. When a trigger fires at block B, the snapshot used is taken at block B + lookback (10 for SmallCurve, 100 for LargeCurve). By the time the execute transaction can land, the request was already armed N blocks earlier, and any flash-loan strategy must hold the inflated balance across the entire lookback window — which atomically defeats the purpose of the flash loan.
Known limits
We make no claim that DualCurve is unbreakable:
- ▸Long-horizon Sybil: an attacker holding multiple wallets for >1h each, each above 0.1% supply, can occupy multiple top-100 slots. Requires meaningful capital but is not infeasible.
- ▸Whale dominance: nothing prevents a small group from holding the majority of top-20 supply by simply buying enough.
- ▸Coordination off-chain: a group could agree to hold and split rewards off-chain; indistinguishable from organic behavior.
- ▸Bridge / wrapper interactions: tokens wrapped by a third-party bridge may not appear in the holder ranking even though they represent the same underlying position.
On-chain ranking trade-offs
The HolderRanking module is the most contentious architectural decision. The alternatives considered were:
| approach | pros | cons |
|---|---|---|
| fully off-chain (indexer + merkle root) | cheap to operate; ranking flexible | trusted root submitter; liveness coupled to indexer; dispute window UX |
| fully on-chain (chosen) | trustless; atomic with distribution; no operator | O(log N) writes per balance-changing transfer; gas cost on heap rebuilds |
We chose fully on-chain. The bounded heap size of 100 keeps the worst-case rebuild at roughly N log N ≈ 700 sift operations, which stays well under realistic Base gas budgets. The token is still fully composable with the rest of DeFi because no logic runs inside _transfer itself — the ranking hook is called via _update at the end of every mint/burn/transfer.
Economic analysis
Rational holder return
For a holder with balance b, where the top-100 collective balance is B, expected return per SmallCurve round is:
E[reward] = 5 ETH · ( b / B )
Daily expected return depends on round frequency, which is volume-dependent (§3.2). For a holder with 0.5% of top-100 supply at $250k daily volume:
E[daily] ≈ 5 ETH · 0.005 · 18 rounds/day ≈ 0.45 ETH/day
from SmallCurve alone. The LargeCurve expected value adds a low-frequency, high-magnitude term that depends on the probability of being in the top 20 when the 500 ETH threshold fires.
Sybil break-even
Fragmentation cost: each additional wallet requires (a) transfer gas (~50,000 gas on Base ≈ $0.01), (b) 0.1% supply minimum, (c) heap rebuild gas paid by everyone interacting with the top-100, and (d) a 1h hold-time delay before eligibility.
For a holder who already controls 5% of supply, fragmenting into five 1%-wallets does not increase total reward — it spreads the same proportional share across five entries. The attacker gains nothing and pays gas. Fragmentation is economically neutral at best, net-negative once gas costs are accounted for.
Security considerations
Reentrancy
All ETH-handling entry points (push transfers in SmallCurve, ETH withdrawals in LargeCurve, FeeRouter forwarding) are protected by OpenZeppelin’s ReentrancyGuard. Push transfers use call{value: a, gas: 30_000} to limit recipient gas. State changes follow CEI before any external call.
v4 hook reentrancy
The Uniswap v4 PoolManager uses a singleton pattern with delta accounting. Calls back into the hook during a swap are by design. The hook never invokes poolManager.swap from within its own callbacks, so reentrant swap loops cannot form.
Hook address mining
The hook’s address must satisfy a specific bit-pattern based on which callbacks it implements (0xCC: BEFORE_SWAP | BEFORE_SWAP_RETURNS_DELTA | AFTER_SWAP | AFTER_SWAP_RETURNS_DELTA). The CREATE2 salt is computed off-chain and the deployment script verifies the resulting address against the expected pattern before proceeding.
No delegatecall, no upgradeability, partial renouncement
None of the contracts use delegatecall or proxy patterns. All deployments are immutable.
Ownership is renounced on the token, the v4 hook, the fee router, and both snapshot managers immediately after initial wiring. The tax rate, the fee split, and the snapshot authorization can never be modified.
SmallCurveDistributor and LargeCurveAccumulator retain their owner for the sole purpose of an inactivity-gated safety valve. The owner can call rescueAccumulated(to, amount) only after RESCUE_INACTIVITY_BLOCKS (302,400 blocks ≈ 7 days on Base) have elapsed since the last deposit from FeeRouter. Any swap on the official pool — including MEV bots, arbitrage, or organic activity — resets the timer, making the function unreachable during normal operation. The function only touches accumulated ETH (the pre-distribution balance) and cannot extract from reclaimable[user] mappings or from active LargeCurve rounds, which remain holder-locked at all times.
This design trades a small amount of credibility (two contracts are not fully renounced) for an explicit safety guarantee that fee ETH cannot be permanently stranded if the protocol becomes completely inactive. The 7-day inactivity trigger is verifiable on-chain by anyone reading lastDepositBlock.
audit status
The current draft is being prepared for external audit. The audit report, when available, will be linked from this document. Pre-audit, treat the protocol as experimental and do not commit funds you cannot afford to lose.
Limitations
- ▸Tax bypass via secondary venues. If a holder swaps DCRV on a venue other than the official v4 pool (third-party AMM, OTC, CEX), no tax is collected. Intentional — taxing all transfers would break composability — but it does mean the reward pools fill only from official-pool volume.
- ▸Threshold rigidity. The 5 ETH and 500 ETH thresholds are constants. If protocol volume is far from the assumed scale, the cadence will be either too fast (rounds fire constantly) or too slow (the large curve never fills). v0.2 may parameterise this.
- ▸Whale dynamics. The mechanism does not prevent concentration; it rewards it. If supply concentrates in few hands, distributions concentrate too. This is a feature for the holders involved but a consideration for newcomers evaluating the system.
- ▸Tax incidence on entry & exit. Buyers pay 5% on entry, sellers pay 5% on exit. This is a friction; holders who never trade pay nothing.
- ▸Heap rebuild gas. A balance change touching a top-100 holder costs O(N log N) sift operations. On Base this is well under a cent at typical gas prices, but on more expensive L2s it would be material.
Future work
The current design is v0.1. Several extensions are under consideration for future versions:
- ▸Separated trigger pattern: decouple threshold-crossing from execution so the swapper who happens to cross the threshold does not bear the gas cost of the entire distribution loop.
- ▸Adaptive thresholds: thresholds that scale with rolling protocol volume, ensuring round cadence remains stable across volume regimes.
- ▸Cross-pool taxation: support multiple official pools (e.g. DCRV/ETH and DCRV/USDC) with unified tax routing.
- ▸Reward boosting via lock: optional ve-token style lockups that boost ranking weight, distinguishing committed long-term holders from passive top-100 occupants.
- ▸Multi-chain deployment: deploy on multiple L2s with cross-chain ranking aggregation.
References
- [1]Uniswap Labs. Uniswap v4 Core Specification. github.com/Uniswap/v4-core
- [2]OpenZeppelin. Contracts library v5. github.com/OpenZeppelin/openzeppelin-contracts
- [3]Vectorized. Solady — gas-optimised Solidity snippets (MinHeapLib). github.com/Vectorized/solady
- [4]Foundry-rs. Foundry. github.com/foundry-rs/foundry
- [5]Reibel, P. et al. Merkle distribution patterns in DeFi. Various airdrop implementations (Uniswap, 1inch, Optimism).