StakingDistributor

0x9bc48b52677c06f9de654ae378406ed8da89677b

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
2,575 bytes
ABI entries
22
15 read · 3 write
License
none

Contract information

Address
0x9bc48b52677c06f9de654ae378406ed8da89677b
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xd4e199dfcc…4e9cc95292

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (15)

accWethPerVe()uint256
accWickPerVe()uint256
carryWeth()uint256
carryWick()uint256
claimWeth(address)uint256
claimWick(address)uint256
claimable(address)uint256, uint256
debtWeth(address)uint256
debtWick(address)uint256
hook()address
shares(address)uint256
totalShares()uint256
ve()address
weth()address
wick()address

Events (2)

ClaimedRewardAdded

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IERC20Minimal",
        "name": "_weth",
        "type": "address"
      },
      {
        "internalType": "contract IERC20Minimal",
        "name": "_wick",
        "type": "address"
      },
      {
        "internalType": "contract IVeWick",
        "name": "_ve",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_hook",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "NotHook",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "weth",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wick",
        "type": "uint256"
      }
    ],
    "name": "Claimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "weth",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "wick",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "shareTotal",
        "type": "uint256"
      }
    ],
    "name": "RewardAdded",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "accWethPerVe",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "accWickPerVe",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "carryWeth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "carryWick",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "checkpoint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "w",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "k",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "claimWeth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "claimWick",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "claimable",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "w",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "k",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "debtWeth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "debtWick",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "hook",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "w",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "k",
        "type": "uint256"
      }
    ],
    "name": "notifyReward",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "shares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalShares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ve",
    "outputs": [
      {
        "internalType": "contract IVeWick",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "weth",
    "outputs": [
      {
        "internalType": "contract IERC20Minimal",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "wick",
    "outputs": [
      {
        "internalType": "contract IERC20Minimal",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

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

interface IVeWick {
    function veBalanceOf(address user) external view returns (uint256);
    function totalVe() external view returns (uint256);
}

/// @notice Distributes the 1% staking fee cut (dual-leg WETH + WICK) to veWICK
/// holders, pro-rata by checkpointed shares. Standard MasterChef accumulator:
/// VeWick calls checkpoint() after every weight change so shares track veWICK.
/// Fees that arrive while totalShares == 0 are carried and folded into the next
/// distribution.
///
/// AUDIT FIX (S-01 defense-in-depth): notifyReward divides by `totalShares`
/// (sum of checkpointed weights), never raw `ve.totalVe()`, so uncheckpointed
/// ve cannot dilute / orphan rewards.
///
/// DRAFT — money-holding, needs audit before mainnet.
contract StakingDistributor {
    uint256 constant P = 1e18; // accumulator precision

    IERC20Minimal public immutable weth;
    IERC20Minimal public immutable wick;
    IVeWick public immutable ve;
    address public immutable hook; // the only address allowed to fund

    uint256 public accWethPerVe;
    uint256 public accWickPerVe;
    uint256 public carryWeth; // fees banked while totalShares == 0
    uint256 public carryWick;
    uint256 public totalShares; // Σ shares[user] — notifyReward divisor

    mapping(address => uint256) public shares; // cached veWICK per user
    mapping(address => uint256) public debtWeth;
    mapping(address => uint256) public debtWick;
    mapping(address => uint256) public claimWeth;
    mapping(address => uint256) public claimWick;

    event RewardAdded(uint256 weth, uint256 wick, uint256 shareTotal);
    event Claimed(address indexed user, uint256 weth, uint256 wick);

    error NotHook();

    constructor(IERC20Minimal _weth, IERC20Minimal _wick, IVeWick _ve, address _hook) {
        weth = _weth;
        wick = _wick;
        ve = _ve;
        hook = _hook;
    }

    /// Hook has already transferred `w` WETH and `k` WICK to this contract.
    function notifyReward(uint256 w, uint256 k) external {
        if (msg.sender != hook) revert NotHook();
        uint256 T = totalShares;
        if (T == 0) {
            carryWeth += w;
            carryWick += k;
            return;
        }
        w += carryWeth;
        k += carryWick;
        carryWeth = 0;
        carryWick = 0;
        accWethPerVe += w * P / T;
        accWickPerVe += k * P / T;
        emit RewardAdded(w, k, T);
    }

    /// Settle `user` to the current accumulator, then re-sync their cached
    /// shares to their live veWICK. Called by VeWick after every weight change,
    /// and internally on claim.
    function checkpoint(address user) public {
        uint256 s = shares[user];
        if (s > 0) {
            claimWeth[user] += s * accWethPerVe / P - debtWeth[user];
            claimWick[user] += s * accWickPerVe / P - debtWick[user];
        }
        uint256 ns = ve.veBalanceOf(user);
        totalShares = totalShares - s + ns;
        shares[user] = ns;
        debtWeth[user] = ns * accWethPerVe / P;
        debtWick[user] = ns * accWickPerVe / P;
    }

    function claim() external returns (uint256 w, uint256 k) {
        checkpoint(msg.sender);
        w = claimWeth[msg.sender];
        k = claimWick[msg.sender];
        claimWeth[msg.sender] = 0;
        claimWick[msg.sender] = 0;
        if (w > 0) weth.transfer(msg.sender, w);
        if (k > 0) wick.transfer(msg.sender, k);
        emit Claimed(msg.sender, w, k);
    }

    /// View of a user's unclaimed WETH + WICK, including unsettled accrual.
    function claimable(address user) external view returns (uint256 w, uint256 k) {
        uint256 s = shares[user];
        w = claimWeth[user] + s * accWethPerVe / P - debtWeth[user];
        k = claimWick[user] + s * accWickPerVe / P - debtWick[user];
    }
}
Chain explorer5849msChain node100ms