ERC-EVOLVE v0.1
Docs / Getting Started / Overview
v0.1.0 Solidity ≥0.8.24 Uniswap v4

ERC-EVOLVE Documentation

A living token standard for the post-v4 era. ERC-EVOLVE extends ERC-20 with per-wallet experience and levels, then uses a Uniswap v4 hook to assign each trader a personalized fee tier on every swap. Long-term holders trade cheaper than short-term flippers — automatically, on-chain, with no trusted intermediary.

Overview

ERC-EVOLVE is composed of two contracts that work together:

To a wallet, block explorer, or DEX aggregator that doesn't know about ERC-EVOLVE, the token behaves exactly like a normal ERC-20. The extended state is exposed through additional view functions and lives entirely on-chain.

NOTE

This is the v0.1 release of the standard. The reference implementation has not yet been audited and should be reviewed before any production deployment.

Why this exists

The ERC-20 standard treats every holder identically. A bot that buys in block 0 and a believer who held since the seed round look the same to the protocol. ERC-EVOLVE introduces a measurable, on-chain notion of conviction: how much you've held, for how long, with what discipline. That conviction translates directly into economic preferential treatment on the canonical trading venue.

Quick start

The fastest path to a working ERC-EVOLVE deployment:

1. Install

Shell
# Add the reference contracts to your Foundry project
forge install OpenZeppelin/openzeppelin-contracts
forge install Uniswap/v4-core
forge install Uniswap/v4-periphery

2. Deploy the token

Solidity
import {ERC20Evolve} from "./ERC20Evolve.sol";

ERC20Evolve token = new ERC20Evolve(
    "Evolve Token",
    "EVO",
    1_000_000_000e18,   // total supply
    treasury                // mint recipient
);

3. Deploy the hook (with address mining)

Solidity
// v4 encodes hook permissions in the deployment address's low bits.
// Mine an address that matches beforeSwap | afterSwap | beforeAddLiquidity.
uint160 flags = uint160(
    Hooks.BEFORE_SWAP_FLAG |
    Hooks.AFTER_SWAP_FLAG |
    Hooks.BEFORE_ADD_LIQUIDITY_FLAG
);

(address hookAddr, bytes32 salt) = HookMiner.find(
    address(this), flags, type(EvolveHook).creationCode,
    abi.encode(poolManager, token)
);

EvolveHook hook = new EvolveHook{salt: salt}(poolManager, token);
require(address(hook) == hookAddr);

token.bindHook(address(hook));

4. Initialize the pool

Solidity
PoolKey memory key = PoolKey({
    currency0: Currency.wrap(address(0)),         // ETH
    currency1: Currency.wrap(address(token)),
    fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,           // hook sets fee per swap
    tickSpacing: 60,
    hooks: IHooks(address(hook))
});

poolManager.initialize(key, startSqrtPriceX96);
TIP

Setting fee: LPFeeLibrary.DYNAMIC_FEE_FLAG is what gives the hook permission to override the fee per-swap. Without it, the hook's fee override will revert.

Installation

ERC-EVOLVE has two runtime dependencies and works with Foundry, Hardhat, and any tool that supports the Solidity standard library.

PackageVersionPurpose
openzeppelin-contracts≥5.0.0ERC20 base, ownership, math
v4-coremainPoolManager, hook types
v4-peripherymainBaseHook helper

Experience points

Every wallet accrues XP whenever it holds a positive token balance. XP is calculated as a time integral of balance:

Math
ΔXP = balance × Δt × BASE_RATE

// where:
//   balance   - the wallet's balance immediately before settlement
//   Δt        - seconds since the wallet's last balance change
//   BASE_RATE - protocol constant (xp per token-second)

XP is settled lazily: the contract only writes new XP to storage when a wallet's balance changes (transfer in, transfer out, or swap). Between settlements, the unsettled XP is computed on-demand by xpOf(account).

Settlement rule

On any balance-changing operation:

  1. Read balanceBefore, lastTouch, and existing xp.
  2. Add balanceBefore × (block.timestamp - lastTouch) × BASE_RATE to xp.
  3. Update lastTouch = block.timestamp.
  4. Apply the balance delta.
  5. If balance decreased, apply the sell penalty.

Levels & tiers

Level is a pure function of XP — a square-root curve that gives quick early progression and slower late progression:

Solidity
function _levelFromXp(uint256 xp) internal view returns (uint16) {
    uint256 lvl = _sqrt(xp / XP_PER_LEVEL_UNIT);
    return lvl > MAX_LEVEL ? MAX_LEVEL : uint16(lvl);
}

The reference tiers — used for narrative purposes in front-ends and metadata — map levels to symbolic stages:

LevelTierDefault rebateNotes
0–4Egg0%New holder; default fee
5–9Hatchling10%~30 days of holding a typical bag
10–19Adolescent25%Unlocks evolved metadata art
20–49Adult50%2× governance voting weight
50Mythic95%Protocol revenue share eligible

Dynamic fees

The hook overrides the pool's fee on every swap. The fee is a linear function of the trader's level:

Solidity
function feeBpsOf(address account) public view returns (uint24) {
    uint16 lvl = levelOf(account);
    if (lvl >= MAX_LEVEL) return MIN_FEE_BPS;
    uint24 range = MAX_FEE_BPS - MIN_FEE_BPS;
    return uint24(MAX_FEE_BPS - (uint256(range) * lvl) / MAX_LEVEL);
}

With the default parameters (MAX_FEE_BPS = 100, MIN_FEE_BPS = 5, MAX_LEVEL = 50), fees range from 1.00% for a fresh wallet to 0.05% at the cap.

WARNING

The pool must be initialized with the DYNAMIC_FEE_FLAG set in PoolKey.fee. A pool initialized with a static fee will revert when the hook tries to override.

Soulbound state

XP and level are tied to the receiving wallet, not to the tokens themselves. When tokens move from A to B:

This makes level-laundering impossible: you cannot buy a "Level 50 bag" from another wallet. The XP belongs to whichever wallet held the tokens through time.

Sell penalty

When a wallet's balance decreases, a fraction of its XP burns. The burn is proportional to the fraction of the bag sold:

Math
XP_burn = currentXP × (Δb / b_old) × (PENALTY_BPS / 10000)

// PENALTY_BPS default: 5000 (= 50%)
// Selling 100% of the bag burns 50% of accumulated XP.
// Selling 50% burns 25%. And so on, linearly.

The intent is to make selling costly without being punitive. A holder who has built up significant reputation should not lose everything in a single forced exit.

Loyalty Yield

A fee discount is a cost reducer — not a reason to hold. ERC-EVOLVE therefore routes a slice of every swap into a Loyalty Treasury that streams continuously to long-term holders.

Fee routing

On every swap through the canonical v4 pool, the hook routes LOYALTY_BPS of the trade size (default: 30 bps = 0.30%) into the treasury. The rest of the fee is paid normally to LPs.

Math
loyaltyTake = swapSize × LOYALTY_BPS / 10000

// At 30 bps and $10M daily volume: ~$30k/day → ~$11M/year
// All of which is paid out to high-XP holders.

Share weight

Each wallet's claim on the treasury is proportional to its balance times a steep tier multiplier. Mythic holders weigh 10× more per token than baseline:

LevelTierMultiplierWhat it means
0–4Egg0No yield. New holders earn XP first.
5–9Hatchling100 (1×)Baseline yield share unlocks
10–19Adolescent250 (2.5×)Yield grows meaningfully
20–49Adult500 (5×)Material passive income
50Mythic1000 (10×)Top of the curve — significantly different

Accumulator pattern

The treasury uses the standard MasterChef-style accumulator. Each deposit advances a global rate per unit weight; each wallet checkpoints its share whenever its weight changes:

Math
// On deposit (called by the hook on every swap):
accLoyaltyPerWeight += deposit / totalLoyaltyWeight

// User's unclaimed yield:
pending = (userWeight × accLoyaltyPerWeight) − rewardDebt
         + pendingHarvest  // crystallized at old weight

// On user balance or level change:
pendingHarvest += oldWeight × accLoyaltyPerWeight − rewardDebt
rewardDebt      = newWeight × accLoyaltyPerWeight

Claiming

Wallets call claimLoyalty() at any time. The treasury holds the asset paid in (typically WETH on the chain); the function transfers the wallet's pending balance and resets the harvest.

Solidity
uint256 claimed = evo.claimLoyalty();
// transfers `claimed` units of LOYALTY_TOKEN to msg.sender
EXAMPLE

At $10M daily volume with the default 30-bp routing and ~10k qualifying wallets, baseline (Lv 5) wallets earn roughly $500/year per equivalent bag, while Lv 50 wallets earn roughly $5,000/year. Yield scales with protocol usage; there is no inflation funding it — snipers and paperhand traders are paying it directly.

Tier perks (beyond yield)

Each tier also unlocks discrete utility, in addition to lower fees and a bigger yield share:

TierFee rebateYield multiplierOther perks
Egg (Lv 0–4)0%
Hatchling (Lv 5–9)10%Soulbound badge, evolved metadata stage 1
Adolescent (Lv 10–19)25%2.5×Launchpad whitelist (partner allocations)
Adult (Lv 20–49)50%2× governance voting weight, NFT badge mint
Mythic (Lv 50)95%10×Treasury proposal rights, exclusive pool access

The fee rebate is enforced by the hook on every swap. The yield multiplier is enforced by the loyalty accumulator on every claim. The discrete perks are enforced by integrating contracts that read levelOf(account) — including the launchpad, governance module, and gated pools.

IERC20Evolve interface

The extension layer adds five view functions and a hook callback on top of standard ERC-20.

Solidity
interface IERC20Evolve is IERC20 {
    // XP / level
    function xpOf(address account) external view returns (uint256);
    function levelOf(address account) external view returns (uint16);
    function lastTouchOf(address account) external view returns (uint64);
    function peakBalanceOf(address account) external view returns (uint256);
    function feeBpsOf(address account) external view returns (uint24);

    // Loyalty Yield
    function tierMultiplier(uint16 level) external pure returns (uint256);
    function loyaltyWeightOf(address account) external view returns (uint256);
    function pendingLoyalty(address account) external view returns (uint256);
    function claimLoyalty() external returns (uint256);

    // Hook callbacks
    function applyTradeEffect(address trader, int256 balanceDelta) external;
    function depositLoyalty(uint256 amount) external;

    event LevelChanged(address indexed account, uint16 oldLevel, uint16 newLevel);
    event XpUpdated(address indexed account, uint256 newXp);
    event LoyaltyDeposited(uint256 amount, uint256 totalWeight);
    event LoyaltyClaimed(address indexed account, uint256 amount);
}

Function reference

FunctionReturnsDescription
xpOfuint256Settled + unsettled XP for the account, in XP units
levelOfuint16Current level (0 to MAX_LEVEL), derived from xpOf
lastTouchOfuint64Unix timestamp of last balance-changing op
peakBalanceOfuint256Highest balance ever held by this wallet
feeBpsOfuint24Effective fee in basis points the account would pay now
applyTradeEffectHook-only callback to record post-swap XP and penalty

ERC20Evolve contract

The reference token implementation extends OpenZeppelin's ERC20 and overrides _update to settle XP on every transfer:

Solidity
function _update(address from, address to, uint256 value) internal override {
    // Settle BEFORE balances change so XP is computed on old balances.
    if (from != address(0)) _settle(_souls[from], balanceOf(from));
    if (to   != address(0)) _settle(_souls[to],   balanceOf(to));

    super._update(from, to, value);

    // Track peak balance + emit level changes.
    if (to != address(0)) {
        Soul storage s = _souls[to];
        if (balanceOf(to) > s.peakBalance) s.peakBalance = balanceOf(to);
        _maybeRecordLevelChange(to, s);
    }
}

Storage layout

FieldTypePurpose
_souls[a].xpuint256Settled XP for account a
_souls[a].lastTouchuint64Last settlement timestamp
_souls[a].peakBalanceuint256All-time high balance
_souls[a].cachedLeveluint16Last emitted level (for LevelChanged)

EvolveHook contract

The hook extends BaseHook from v4-periphery and declares three permissions:

Solidity
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
    return Hooks.Permissions({
        beforeSwap: true,
        afterSwap: true,
        beforeAddLiquidity: true,
        // all others: false
        beforeInitialize: false, afterInitialize: false,
        afterAddLiquidity: false, beforeRemoveLiquidity: false,
        afterRemoveLiquidity: false, beforeDonate: false,
        afterDonate: false, beforeSwapReturnDelta: false,
        afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false,
        afterRemoveLiquidityReturnDelta: false
    });
}

beforeSwap: set personal fee

The hook reads tx.origin as the trader identity and returns a fee with the override flag set:

Solidity
function _beforeSwap(/* ... */) internal override
    returns (bytes4, BeforeSwapDelta, uint24)
{
    address trader = tx.origin;
    uint24 fee = token.feeBpsOf(trader) | LPFeeLibrary.OVERRIDE_FEE_FLAG;
    return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee);
}

afterSwap: update XP & apply penalty

Solidity
function _afterSwap(/* ... */, BalanceDelta delta, /* ... */)
    internal override returns (bytes4, int128)
{
    bool tokenIsC0 = address(token) == Currency.unwrap(key.currency0);
    int256 d = tokenIsC0 ? int256(delta.amount0()) : int256(delta.amount1());
    token.applyTradeEffect(tx.origin, d);
    return (BaseHook.afterSwap.selector, 0);
}

Events

EventEmitted when
XpUpdated(address account, uint256 newXp)Any balance change for account
LevelChanged(address account, uint16 old, uint16 new)The account's level crosses a boundary
Transfer (ERC-20)Standard token transfer event

Errors

ErrorCause
OnlyHook()An address other than the bound hook called applyTradeEffect
HookAlreadySet()bindHook called more than once

Deployment guide

A canonical ERC-EVOLVE deployment proceeds in five steps:

  1. Deploy ERC20Evolve with desired name, symbol, supply, and treasury.
  2. Mine an address for EvolveHook whose low bits match the required permission flags.
  3. Deploy EvolveHook with the mined salt, pointing at the token + PoolManager.
  4. Call token.bindHook(hookAddress) — irreversible.
  5. Initialize the v4 pool with fee: DYNAMIC_FEE_FLAG and hooks: hookAddress.

Foundry script

Solidity
contract Deploy is Script {
    function run() external {
        vm.startBroadcast();
        ERC20Evolve token = new ERC20Evolve("Evolve", "EVO", 1e27, msg.sender);

        uint160 flags = uint160(
            Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG
        );
        (address hookAddr, bytes32 salt) = HookMiner.find(CREATE2_DEPLOYER, flags,
            type(EvolveHook).creationCode, abi.encode(POOL_MANAGER, token));
        EvolveHook hook = new EvolveHook{salt: salt}(POOL_MANAGER, token);
        require(address(hook) == hookAddr, "hook mining failed");

        token.bindHook(address(hook));
        vm.stopBroadcast();
    }
}

Frontend reads

Display a wallet's level and effective fee using the extension view functions:

TypeScript
import { createPublicClient, http, formatUnits } from 'viem';
import { mainnet } from 'viem/chains';
import { evolveAbi } from './abi';

const client = createPublicClient({ chain: mainnet, transport: http() });

const [xp, level, fee] = await client.multicall({
  contracts: [
    { address: EVOLVE, abi: evolveAbi, functionName: 'xpOf', args: [user] },
    { address: EVOLVE, abi: evolveAbi, functionName: 'levelOf', args: [user] },
    { address: EVOLVE, abi: evolveAbi, functionName: 'feeBpsOf', args: [user] },
  ],
});

console.log(`Level ${level.result}, fee ${Number(fee.result) / 100}%`);
TIP

xpOf includes unsettled XP — the value will grow each second even when the wallet hasn't transacted. For displays that update in real-time, just re-poll every few seconds rather than indexing every block.

Routers & aggregators

The hook reads tx.origin to identify the trader. This has implications for trades routed through aggregators or batch contracts:

CAVEAT

EIP-3074 / EIP-7702 delegation may change how tx.origin behaves. Implementations targeting post-3074 chains should consider an alternative trader-identification strategy (e.g., per-pool whitelisted resolvers).

Security model

Sybil resistance

Splitting a bag across N wallets produces no XP advantage. XP is balance × time; the sum across partitions equals the sum of one unified bag held for the same time. Sybilling is economically neutral.

Re-entrancy

The hook calls back into the token contract on afterSwap. The reference implementation follows checks-effects-interactions and the token's state changes are idempotent under the hook's call pattern. Production deployments should add an explicit re-entrancy guard on applyTradeEffect.

MEV

Personalized fees create a new MEV surface: a searcher with a high-level wallet could lend its level out to capture a fee discount. Mitigation: the lastTouch cooldown discounts the effective level of a wallet that received a large incoming transfer within the same block, preventing borrow-and-trade attacks.

Hook address mining

v4 enforces hook permissions through the deployment address. A misconfigured deployment will fail at pool initialization. Always verify with address(hook).codehash and the expected permission bits before binding.

Audit status

ComponentStatus
ERC20EvolveUnaudited (v0.1)
EvolveHookUnaudited (v0.1)
Interface freezePending review

Governance

The reference implementation freezes all parameters at deployment. Production deployments may want to expose:

These should be gated by a timelock and DAO vote. Changing them mid-flight does not retroactively re-compute existing XP — the new rate takes effect from the next settlement onward.

FAQ

Does this break my ERC-20 integrations?

No. ERC-EVOLVE is a strict superset of ERC-20. All standard methods (transfer, approve, balanceOf, etc.) work as expected. Wallets and explorers see a normal token.

Can I "buy" a level?

No. Levels are tied to the wallet, not the tokens. Buying a bag from a high-level wallet starts your XP from your wallet's current XP, not theirs.

What happens if I send tokens to a new wallet?

The receiving wallet starts accruing XP from zero (or its existing XP, if it has any). The sender keeps their XP (minus the sell-penalty for the amount transferred out).

Do LPs earn XP?

Yes — the hook's beforeAddLiquidity permission lets LPs accrue XP at a separate rate. The reference implementation settles LP XP on adds; production deployments can apply a multiplier.

Why tx.origin?

It's the simplest way to identify the actual trader through router intermediaries. The trade-offs are discussed in Routers & aggregators. Implementations on chains that have moved past tx.origin semantics will need a different scheme.

Can I use this without Uniswap v4?

The token alone (without the hook) gives you the XP/level extension — but the killer feature (personalized fees enforced at the AMM) requires v4. On v3 or v2, you can still display levels and use them for off-chain logic, but you cannot price trades by level.

Is this the same as a reflection token?

No. Reflection tokens redistribute fees to all holders on every transfer, applied to the token itself. ERC-EVOLVE leaves the token clean — the conditional behavior lives in the hook, which only fires on the canonical pool. Routing through any other venue costs nothing extra.