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