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
- Creator
- 0xfc4f0b5861…d0c62137FD
- 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