VeWick

0x8ba1414e2fca9790ed1f2684a6a77450d471c198

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
3,842 bytes
ABI entries
36
19 read · 3 write
License
none

Contract information

Address
0x8ba1414e2fca9790ed1f2684a6a77450d471c198
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xdb5c1c1c87…72fde4bcdf

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (19)

BOOST_MAX()uint256
BOOST_MIN()uint256
FOREVER()uint32
TIER_DUR(uint256)uint32
TIER_MULT(uint256)uint64
VE_MAX()uint256
WAD()uint256
admin()address
boostFromVe(uint256)uint256
boostOf(address)uint256
distributor()address
foreverLockedOf(address)uint256
lockCount(address)uint256
locksOf(address, uint256)uint128, uint32, uint32, uint64, bool
totalLocked()uint256
totalVe()uint256
veBalanceOf(address)uint256
veOfLock(tuple)uint256
wick()address

Events (3)

DistributorWiredLockedWithdrawn

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IERC20Minimal",
        "name": "_wick",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_admin",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyOut",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadDistributor",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "BadTier",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DistributorSet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "IsForever",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoDistributor",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoLock",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAdmin",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "StillLocked",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "distributor",
        "type": "address"
      }
    ],
    "name": "DistributorWired",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lockId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint32",
        "name": "end",
        "type": "uint32"
      },
      {
        "indexed": false,
        "internalType": "uint64",
        "name": "mult",
        "type": "uint64"
      }
    ],
    "name": "Locked",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lockId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Withdrawn",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BOOST_MAX",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "BOOST_MIN",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FOREVER",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "TIER_DUR",
    "outputs": [
      {
        "internalType": "uint32",
        "name": "",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "TIER_MULT",
    "outputs": [
      {
        "internalType": "uint64",
        "name": "",
        "type": "uint64"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VE_MAX",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WAD",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "admin",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "ve",
        "type": "uint256"
      }
    ],
    "name": "boostFromVe",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "boostOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distributor",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "foreverLockedOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amt",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint128",
        "name": "amount",
        "type": "uint128"
      },
      {
        "internalType": "uint8",
        "name": "tier",
        "type": "uint8"
      }
    ],
    "name": "lock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "lockId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "lockCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "locksOf",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "amount",
        "type": "uint128"
      },
      {
        "internalType": "uint32",
        "name": "start",
        "type": "uint32"
      },
      {
        "internalType": "uint32",
        "name": "end",
        "type": "uint32"
      },
      {
        "internalType": "uint64",
        "name": "mult",
        "type": "uint64"
      },
      {
        "internalType": "bool",
        "name": "withdrawn",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "d",
        "type": "address"
      }
    ],
    "name": "setDistributor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalLocked",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalVe",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "veBalanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ve",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint128",
            "name": "amount",
            "type": "uint128"
          },
          {
            "internalType": "uint32",
            "name": "start",
            "type": "uint32"
          },
          {
            "internalType": "uint32",
            "name": "end",
            "type": "uint32"
          },
          {
            "internalType": "uint64",
            "name": "mult",
            "type": "uint64"
          },
          {
            "internalType": "bool",
            "name": "withdrawn",
            "type": "bool"
          }
        ],
        "internalType": "struct VeWick.Lock",
        "name": "L",
        "type": "tuple"
      }
    ],
    "name": "veOfLock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "wick",
    "outputs": [
      {
        "internalType": "contract IERC20Minimal",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "lockId",
        "type": "uint256"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import {IERC20Minimal} from "v4-core/src/interfaces/external/IERC20Minimal.sol";

/// @notice veWICK — lock $WICK for a duration to earn a leaderboard score
/// boost and (via the distributor) a share of trading fees. One lock produces
/// veWICK = amount × lockMultiplier(duration); veWICK decays back toward the
/// base (1×) weight as a timed lock nears expiry. FOREVER locks never decay and
/// are irreversible — principal never returns, so they count as removed supply
/// (shown next to burns).
///
/// DESIGN (owner-locked 2026-07-21): score boost caps at 3× at 1% of supply
/// locked forever, linear ramp, fixed cap (not TVL-scaled). Yield accounting
/// lives in a separate distributor; this contract is the veWICK source of truth.
///
/// AUDIT FIXES (S-01 / S-02):
///  - lock() reverts until setDistributor has run (no pre-wiring ve without shares)
///  - setDistributor validates contract code + probes checkpoint(address(0))
///  - withdraw() best-effort checkpoints so a broken distributor cannot freeze principal
///
/// DRAFT — money-holding, needs audit before mainnet. Boost half is gated on the
/// offline-solver fix (see project notes) and a legal opinion.
contract VeWick {
    // ---------------------------------------------------------------- types

    struct Lock {
        uint128 amount; // $WICK principal
        uint32 start; // lock timestamp
        uint32 end; // unlock timestamp; FOREVER = type(uint32).max
        uint64 mult; // lockMultiplier at 1e18 scale (1.1e18 … 10e18)
        bool withdrawn;
    }

    // ------------------------------------------------------------ constants

    uint256 public constant WAD = 1e18;
    uint32 public constant FOREVER = type(uint32).max;

    /// 1% of the 1B supply, locked forever (×3), is the veWICK that maxes boost.
    /// The lock multiplier is capped at 3× so ONE 3× ceiling governs both the
    /// yield weight and the score boost (owner call 2026-07-22 — was ×10 weight).
    /// VE_MAX = (10_000_000e18) × 3 = 3e25.
    uint256 public constant VE_MAX = 30_000_000e18;
    uint256 public constant BOOST_MIN = 1e18; // 1×
    uint256 public constant BOOST_MAX = 3e18; // 3× at/above VE_MAX

    // duration options (seconds) and their multipliers (1e18). Index = tier id.
    // FOREVER is tier 6 (handled separately).
    uint32[6] public TIER_DUR =
        [uint32(1 days), 7 days, 30 days, 90 days, 365 days, 365 days];

    // 1.1× / 1.25× / 1.5× / 1.9× / 2.4× / (forever 3×) — ceiling 3×, same as boost
    uint64[7] public TIER_MULT = [
        uint64(1.1e18),
        1.25e18,
        1.5e18,
        1.9e18,
        2.4e18,
        2.4e18, // tier 5 unused placeholder to keep arrays aligned
        3e18 // FOREVER multiplier
    ];

    // ------------------------------------------------------------- immutable

    IERC20Minimal public immutable wick;
    address public immutable admin;
    address public distributor; // yield accumulator; set once, notified on every weight change

    // ----------------------------------------------------------------- state

    mapping(address => Lock[]) public locksOf;
    uint256 public totalLocked; // sum of live principal
    uint256 public totalVe; // sum of live veWICK (drives the distributor)

    // ---------------------------------------------------------------- events

    event Locked(address indexed user, uint256 lockId, uint256 amount, uint32 end, uint64 mult);
    event Withdrawn(address indexed user, uint256 lockId, uint256 amount);
    event DistributorWired(address indexed distributor);

    // ---------------------------------------------------------------- errors

    error BadTier();
    error ZeroAmount();
    error StillLocked();
    error IsForever();
    error AlreadyOut();
    error NoLock();
    error NotAdmin();
    error DistributorSet();
    error NoDistributor();
    error BadDistributor();

    constructor(IERC20Minimal _wick, address _admin) {
        wick = _wick;
        admin = _admin;
    }

    /// one-time wiring: VeWick and the distributor reference each other, so the
    /// distributor is deployed second and set here once. Rejects EOAs / empty
    /// accounts and probes checkpoint so a non-compliant target cannot be latched
    /// (audit S-02).
    function setDistributor(address d) external {
        if (msg.sender != admin) revert NotAdmin();
        if (distributor != address(0)) revert DistributorSet();
        if (d == address(0) || d.code.length == 0) revert BadDistributor();
        // Probe: must implement checkpoint and not revert on a zero-weight user.
        IStakingCheckpoint(d).checkpoint(address(0));
        distributor = d;
        emit DistributorWired(d);
    }

    // ------------------------------------------------------------- mutations

    /// tier 0..4 = timed (1d/1w/1m/3m/1y), tier 6 = FOREVER. (tier 5 reserved.)
    /// Requires distributor wired first so every lock is checkpointed into shares
    /// (audit S-01 — no pre-wiring totalVe without shares).
    function lock(uint128 amount, uint8 tier) external returns (uint256 lockId) {
        if (distributor == address(0)) revert NoDistributor();
        if (amount == 0) revert ZeroAmount();
        bool forever = tier == 6;
        if (!forever && tier > 4) revert BadTier();

        wick.transferFrom(msg.sender, address(this), amount);
        uint32 nowT = uint32(block.timestamp);
        uint32 end = forever ? FOREVER : nowT + TIER_DUR[tier];
        uint64 mult = TIER_MULT[tier];

        lockId = locksOf[msg.sender].length;
        locksOf[msg.sender].push(Lock({amount: amount, start: nowT, end: end, mult: mult, withdrawn: false}));
        totalLocked += amount;
        totalVe += uint256(amount) * mult / WAD;
        emit Locked(msg.sender, lockId, amount, end, mult);
        // Strict: a failing distributor must not accept new locks (shares sync required).
        IStakingCheckpoint(distributor).checkpoint(msg.sender);
    }

    /// Withdraw a matured timed lock. FOREVER locks can never be withdrawn.
    /// Checkpoint is best-effort so a broken distributor cannot freeze principal
    /// (audit S-02 escape hatch).
    function withdraw(uint256 lockId) external {
        Lock[] storage ls = locksOf[msg.sender];
        if (lockId >= ls.length) revert NoLock();
        Lock storage L = ls[lockId];
        if (L.withdrawn) revert AlreadyOut();
        if (L.end == FOREVER) revert IsForever();
        if (block.timestamp < L.end) revert StillLocked();

        L.withdrawn = true;
        totalLocked -= L.amount;
        totalVe -= uint256(L.amount) * L.mult / WAD;
        _pingBestEffort(msg.sender);
        wick.transfer(msg.sender, L.amount);
        emit Withdrawn(msg.sender, lockId, L.amount);
    }

    // ----------------------------------------------------------------- views

    /// veWICK of a single lock = amount × lockMultiplier, constant while the
    /// lock is active (v1: no decay — see contract header). Zero once withdrawn.
    /// This is what makes the distributor's accumulator provably correct: a
    /// user's weight only changes on lock() / withdraw(), never passively.
    function veOfLock(Lock memory L) public pure returns (uint256) {
        if (L.withdrawn) return 0;
        return uint256(L.amount) * L.mult / WAD;
    }

    function veBalanceOf(address user) public view returns (uint256 ve) {
        Lock[] storage ls = locksOf[user];
        for (uint256 i = 0; i < ls.length; i++) ve += veOfLock(ls[i]);
    }

    /// Score/yield boost at 1e18 scale (1e18 … 3e18). Linear ramp, capped at
    /// 3× once veWICK reaches VE_MAX (1% supply forever).
    ///
    /// Settlement: MUST NOT be written into the merkle leaf `score` —
    /// `WickHook.claim` requires leaf.score == sum of on-chain run scores.
    /// Off-chain root-poster may use boostOf to assign ranks; payout is
    /// rank-based (`rankBps`) only.
    function boostOf(address user) external view returns (uint256) {
        return boostFromVe(veBalanceOf(user));
    }

    function boostFromVe(uint256 ve) public pure returns (uint256) {
        uint256 capped = ve > VE_MAX ? VE_MAX : ve;
        return BOOST_MIN + (BOOST_MAX - BOOST_MIN) * capped / VE_MAX;
    }

    /// Principal a user has locked FOREVER — the burn-adjacent supply sink.
    function foreverLockedOf(address user) public view returns (uint256 amt) {
        Lock[] storage ls = locksOf[user];
        for (uint256 i = 0; i < ls.length; i++) {
            if (!ls[i].withdrawn && ls[i].end == FOREVER) amt += ls[i].amount;
        }
    }

    function lockCount(address user) external view returns (uint256) {
        return locksOf[user].length;
    }

    // ---------------------------------------------------------------- hooks

    /// Best-effort settle after withdraw. Failure must not trap principal.
    function _pingBestEffort(address user) internal {
        if (distributor == address(0)) return;
        try IStakingCheckpoint(distributor).checkpoint(user) {} catch {}
    }
}

interface IStakingCheckpoint {
    function checkpoint(address user) external;
}
Chain explorer3166msChain node69ms