PadQuiverDistributor
0xe94588a1944c1f91fd2488e3b4821e66584a1781
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
11,575 bytes
ABI entries
47
33 read · 7 write
License
none
Contract information
- Address
- 0xe94588a1944c1f91fd2488e3b4821e66584a1781
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x3c95C4ab74…FD8D1Eaa02
- Creation tx
- 0x3abdd2dbb7…427bd8351c
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (33)
FEE_DENOMINATOR() → uint256
WEBSITE() → string
accumulativeDividendOf(address) → uint256
callerTipBps() → uint256
excludedFromDividends(address) → bool
factory() → address
holderCount() → uint256
lastProcessedIndex() → uint256
magnifiedDividendPerShare() → uint256
maxSlippageBps() → uint256
minimumBalanceForDividends() → uint256
onERC721Received(address, address, uint256, bytes) → bytes4
pendingDistribution() → uint256
platformFeeBps() → uint256
platformWallet() → address
pool() → address
poolFee() → uint24
positionManager() → address
positionTokenId() → uint256
rewardPool() → address
rewardPoolFee() → uint24
rewardToken() → address
shares(address) → uint256
swapRouter() → address
token() → address
tokenIsToken0() → bool
totalDividendsDistributed() → uint256
totalDividendsWithdrawn() → uint256
totalShares() → uint256
twapWindow() → uint32
weth() → address
withdrawableDividendOf(address) → uint256
withdrawnDividends(address) → uint256
Events (4)
DividendWithdrawFailedDividendWithdrawnDividendsDistributedHarvested
ABI
[
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "factory",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "swapRouter",
"type": "address"
},
{
"internalType": "address",
"name": "weth",
"type": "address"
},
{
"internalType": "address",
"name": "rewardToken",
"type": "address"
},
{
"internalType": "address",
"name": "pool",
"type": "address"
},
{
"internalType": "address",
"name": "rewardPool",
"type": "address"
},
{
"internalType": "uint24",
"name": "poolFee",
"type": "uint24"
},
{
"internalType": "uint24",
"name": "rewardPoolFee",
"type": "uint24"
},
{
"internalType": "bool",
"name": "tokenIsToken0",
"type": "bool"
},
{
"internalType": "address",
"name": "platformWallet",
"type": "address"
},
{
"internalType": "uint256",
"name": "platformFeeBps",
"type": "uint256"
},
{
"internalType": "uint32",
"name": "twapWindow",
"type": "uint32"
},
{
"internalType": "uint256",
"name": "maxSlippageBps",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "callerTipBps",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minimumBalanceForDividends",
"type": "uint256"
}
],
"internalType": "struct PadQuiverDistributor.Config",
"name": "c",
"type": "tuple"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DividendWithdrawFailed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DividendWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DividendsDistributed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "tokenCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "wethCollected",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "platformCut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "callerTip",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "rewardsDistributed",
"type": "uint256"
}
],
"name": "Harvested",
"type": "event"
},
{
"inputs": [],
"name": "FEE_DENOMINATOR",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "WEBSITE",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "accumulativeDividendOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "callerTipBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "claim",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bool",
"name": "excluded",
"type": "bool"
}
],
"name": "excludeFromDividends",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "excludedFromDividends",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "harvest",
"outputs": [
{
"internalType": "uint256",
"name": "rewardsDistributed",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "holderCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "lastProcessedIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "magnifiedDividendPerShare",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxSlippageBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "minimumBalanceForDividends",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "pendingDistribution",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformFeeBps",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformWallet",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolFee",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "contract INonfungiblePositionManager",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionTokenId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "gasLimit",
"type": "uint256"
}
],
"name": "process",
"outputs": [
{
"internalType": "uint256",
"name": "iterations",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "claims",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "processAccount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "rewardPool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "rewardPoolFee",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "rewardToken",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
}
],
"name": "setShare",
"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": "swapRouter",
"outputs": [
{
"internalType": "contract ISwapRouter",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tokenIsToken0",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalDividendsDistributed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalDividendsWithdrawn",
"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": "twapWindow",
"outputs": [
{
"internalType": "uint32",
"name": "",
"type": "uint32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "withdrawableDividendOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "withdrawnDividends",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import {INonfungiblePositionManager, ISwapRouter} from "./interfaces/IUniswapV3.sol";
import {V3Twap} from "./libraries/V3Twap.sol";
interface IPadQuiverFactory {
function owner() external view returns (address);
}
/**
* @title PadQuiverDistributor
* @author PadQuiver Launchpad — https://padquiver.com/
* @custom:website https://padquiver.com/
* @notice The reward engine for a PadQuiverToken. Holds the position NFT (liquidity
* PERMANENTLY LOCKED). BOT-FREE / OPERATOR-FREE design:
* - harvest() is PUBLIC (no keeper).
* - Swaps compute the minimum output THEMSELVES from the pool's TWAP => any atomic
* self-sandwich PROFIT is BOUNDED by ~maxSlippageBps (not zero); no trusted
* keeper is needed. (On thin reward pools the window/tolerance matter.)
* - A small tip is paid to whoever triggers harvest => the market triggers it, no bot needed.
* - There is no owner/admin (the platform can only perform secondary-pool exclusion).
* Reward accounting follows the CCAT DividendTracker (saturating pending, skip-if-fail).
*/
contract PadQuiverDistributor is ReentrancyGuard, IERC721Receiver {
using SafeERC20 for IERC20;
/// @notice Official PadQuiver launchpad website (readable on-chain).
string public constant WEBSITE = "https://padquiver.com/";
uint256 private constant MAGNITUDE = 2 ** 128;
uint256 public constant FEE_DENOMINATOR = 10_000;
address private constant DEAD = 0x000000000000000000000000000000000000dEaD;
INonfungiblePositionManager public immutable positionManager;
ISwapRouter public immutable swapRouter;
address public immutable token;
address public immutable weth;
address public immutable rewardToken;
address public immutable pool; // token/WETH pool
address public immutable rewardPool; // WETH/rewardToken pool (0 if rewardToken == WETH)
uint24 public immutable poolFee;
uint24 public immutable rewardPoolFee;
bool public immutable tokenIsToken0;
address public immutable platformWallet;
uint256 public immutable platformFeeBps;
address public immutable factory; // platform admin (secondary-pool exclusion) comes from here
uint32 public immutable twapWindow; // seconds
uint256 public immutable maxSlippageBps; // allowed deviation from TWAP
uint256 public immutable callerTipBps; // reward to whoever calls harvest
// Reward threshold: only holders ABOVE this balance earn rewards (creator-chosen).
// Low = everyone; high = only large holders. Uses the trustless magnified model.
uint256 public immutable minimumBalanceForDividends;
uint256 public positionTokenId;
bool private _initialized;
uint256 public totalShares;
uint256 public magnifiedDividendPerShare;
uint256 public totalDividendsDistributed;
uint256 public totalDividendsWithdrawn;
mapping(address => uint256) public shares;
mapping(address => int256) private magnifiedDividendCorrections;
mapping(address => uint256) public withdrawnDividends;
mapping(address => bool) public excludedFromDividends;
address[] private holders;
mapping(address => uint256) private holderIndexPlusOne;
uint256 public lastProcessedIndex;
// Post-fee WETH from a harvest whose reward-swap couldn't complete (already taxed) — carried, never re-taxed.
uint256 private taxedWethPending;
// Reward tokens genuinely produced by harvests but not yet distributed (no eligible holders yet).
// Tracked explicitly so a DONATION of the reward token can never be counted as reward / inflate perShare.
uint256 private pendingReward;
// Collected token whose token->WETH swap couldn't complete this harvest — carried and retried next
// time (symmetric with taxedWethPending) so a partial/chunked swap never permanently strands fees.
uint256 private tokenPending;
event Harvested(uint256 tokenCollected, uint256 wethCollected, uint256 platformCut, uint256 callerTip, uint256 rewardsDistributed);
event DividendsDistributed(uint256 amount);
event DividendWithdrawn(address indexed account, uint256 amount);
event DividendWithdrawFailed(address indexed account, uint256 amount);
modifier onlyToken() {
require(msg.sender == token, "RRD: only token");
_;
}
struct Config {
address factory; // the real PadQuiverFactory (not the deployer)
address token;
address positionManager;
address swapRouter;
address weth;
address rewardToken;
address pool;
address rewardPool;
uint24 poolFee;
uint24 rewardPoolFee;
bool tokenIsToken0;
address platformWallet;
uint256 platformFeeBps;
uint32 twapWindow;
uint256 maxSlippageBps;
uint256 callerTipBps;
uint256 minimumBalanceForDividends;
}
constructor(Config memory c) {
require(c.token != address(0) && c.rewardToken != address(0) && c.pool != address(0), "RRD: zero");
require(c.platformFeeBps <= 2_000, "RRD: platform fee too high");
require(c.maxSlippageBps <= 1_000, "RRD: slippage too high"); // <= 10%
require(c.callerTipBps <= 100, "RRD: tip too high"); // <= 1%
require(c.twapWindow >= 60 && c.twapWindow <= 3600, "RRD: bad window");
require(c.factory != address(0), "RRD: zero factory");
factory = c.factory;
token = c.token;
positionManager = INonfungiblePositionManager(c.positionManager);
swapRouter = ISwapRouter(c.swapRouter);
weth = c.weth;
rewardToken = c.rewardToken;
pool = c.pool;
rewardPool = c.rewardPool;
poolFee = c.poolFee;
rewardPoolFee = c.rewardPoolFee;
tokenIsToken0 = c.tokenIsToken0;
platformWallet = c.platformWallet;
platformFeeBps = c.platformFeeBps;
twapWindow = c.twapWindow;
maxSlippageBps = c.maxSlippageBps;
callerTipBps = c.callerTipBps;
minimumBalanceForDividends = c.minimumBalanceForDividends;
excludedFromDividends[c.pool] = true;
excludedFromDividends[c.positionManager] = true;
excludedFromDividends[c.swapRouter] = true;
excludedFromDividends[address(this)] = true;
excludedFromDividends[c.factory] = true;
excludedFromDividends[address(0)] = true;
excludedFromDividends[DEAD] = true;
}
function initialize(uint256 tokenId) external {
require(msg.sender == factory, "RRD: only factory");
require(!_initialized, "RRD: initialized");
require(positionManager.ownerOf(tokenId) == address(this), "RRD: not position owner");
_initialized = true;
positionTokenId = tokenId;
}
/// @notice Excludes addresses that shouldn't earn rewards, such as secondary pools/contracts.
/// Only the PLATFORM (factory owner) — NOT the customer (this prevents a rug).
/// SECURITY (M1): only CONTRACT addresses can be excluded; the owner cannot exclude
/// real (EOA) holders and redirect the rewards to a single wallet.
function excludeFromDividends(address account, bool excluded) external {
require(msg.sender == IPadQuiverFactory(factory).owner(), "RRD: only platform");
// System addresses are PERMANENTLY excluded in the constructor and can NEVER be toggled. This
// stops an owner (by accident or malice) re-including the pool — which would balloon totalShares
// to the whole supply and permanently strand every holder's rewards in the pool address.
require(
account != pool && account != address(positionManager) && account != address(swapRouter)
&& account != factory && account != address(this) && account != DEAD && account != address(0),
"RRD: system address is permanently excluded"
);
require(!excluded || account.code.length > 0, "RRD: can only exclude contracts");
excludedFromDividends[account] = excluded;
if (excluded && shares[account] > 0) {
uint256 oldShare = shares[account];
magnifiedDividendCorrections[account] += int256(magnifiedDividendPerShare * oldShare);
totalShares -= oldShare;
shares[account] = 0;
_removeHolder(account);
}
}
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return IERC721Receiver.onERC721Received.selector;
}
// ---------------------------------------------------------------------
// Harvest — PUBLIC, TWAP-protected, tips the caller
// ---------------------------------------------------------------------
/// @notice Collects the accrued LP fees, converts them into the reward coin via
/// TWAP-protected swaps, tips the caller, and distributes to holders. No bot needed.
function harvest() external nonReentrant returns (uint256 rewardsDistributed) {
require(_initialized, "RRD: not initialized");
(uint256 amount0, uint256 amount1) = positionManager.collect(
INonfungiblePositionManager.CollectParams({
tokenId: positionTokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
uint256 tokenCollected = tokenIsToken0 ? amount0 : amount1;
uint256 wethCollected = tokenIsToken0 ? amount1 : amount0;
// 1) Convert ONLY the token actually COLLECTED into WETH. Bounding by tokenCollected (not
// balanceOf) means a donated token balance can neither be counted as reward nor force an
// oversized swap; the swap is chunked so a huge amount can never "revert forever".
uint256 swapTok = tokenCollected + tokenPending; // + token a prior harvest couldn't swap yet
uint256 tokBal = IERC20(token).balanceOf(address(this));
if (swapTok > tokBal) swapTok = tokBal; // (also caps at balance => a token donation is excluded)
uint256 wethFromToken;
if (swapTok > 0) {
uint256 tokUsed;
(tokUsed, wethFromToken) = _swapTwapProtected(pool, token, weth, poolFee, swapTok);
tokenPending = swapTok - tokUsed; // unswapped collected token carried to the next harvest
} else {
tokenPending = 0;
}
uint256 newWeth = wethCollected + wethFromToken; // WETH genuinely produced this harvest
// 2) Platform fee ONLY on newly-produced WETH (carried/donated WETH is never re-taxed), then
// turn the post-fee WETH into the reward token.
// No platform wallet => take NO cut at all (otherwise it would be subtracted from rewards but
// never transferred, stranding that WETH). With a wallet, fee is on newly-produced WETH only.
uint256 platformCut = platformWallet == address(0) ? 0 : (newWeth * platformFeeBps) / FEE_DENOMINATOR;
if (platformCut > 0) {
IERC20(weth).safeTransfer(platformWallet, platformCut);
}
uint256 producedReward;
if (rewardToken == weth) {
producedReward = newWeth - platformCut;
} else {
uint256 toSwap = (newWeth - platformCut) + taxedWethPending; // + WETH carried from a prior failed swap
uint256 wethUsed;
if (toSwap > 0) {
(wethUsed, producedReward) = _swapTwapProtected(rewardPool, weth, rewardToken, rewardPoolFee, toSwap);
}
taxedWethPending = toSwap - wethUsed; // unswapped post-fee WETH carried to next harvest (already taxed)
}
// 3) Distribute ONLY the reward genuinely produced (+ any held from a no-holders window). A
// donation of the reward token is NOT counted here, so it can never inflate perShare.
uint256 distributable = producedReward + pendingReward;
pendingReward = 0;
uint256 tip;
if (distributable > 0 && totalShares > 0) {
tip = (distributable * callerTipBps) / FEE_DENOMINATOR;
if (tip > 0) IERC20(rewardToken).safeTransfer(msg.sender, tip);
rewardsDistributed = distributable - tip;
magnifiedDividendPerShare += (rewardsDistributed * MAGNITUDE) / totalShares;
totalDividendsDistributed += rewardsDistributed;
emit DividendsDistributed(rewardsDistributed);
} else if (distributable > 0) {
// no eligible holders yet: hold it for the next harvest (not lost, not re-taxed, no tip)
pendingReward = distributable;
}
emit Harvested(tokenCollected, wethCollected, platformCut, tip, rewardsDistributed);
}
/// @dev Computes minOut from the pool's TWAP (time-weighted => resistant to atomic
/// manipulation). If there isn't enough oracle history, consultTick reverts (harvest
/// is too early; it's retried later). If the swap reverts due to price deviation, the
/// revert is swallowed, the token stays inside, and the next harvest retries.
function _swapTwapProtected(address twapPool, address tokenIn, address tokenOut, uint24 fee, uint256 amountIn)
private
returns (uint256 amountConsumed, uint256 amountOut)
{
uint256 attempt = amountIn;
// Try the full amount; if the TWAP-bounded swap reverts (price deviation, or the amount is too
// large for the pool within the slippage cap — e.g. a griefer's oversized donation), halve and
// retry. Whatever swaps, swaps; the unswapped remainder is carried to the next harvest, so a
// too-large balance can never make harvest "revert forever".
for (uint256 i = 0; i < 6; i++) {
if (attempt == 0) break;
uint256 minOut = V3Twap.minOutFromTwap(twapPool, twapWindow, attempt, tokenIn, tokenOut, maxSlippageBps);
IERC20(tokenIn).forceApprove(address(swapRouter), attempt);
try swapRouter.exactInputSingle(
ISwapRouter.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: fee,
recipient: address(this),
deadline: block.timestamp,
amountIn: attempt,
amountOutMinimum: minOut,
sqrtPriceLimitX96: 0
})
) returns (uint256 out) {
IERC20(tokenIn).forceApprove(address(swapRouter), 0);
return (attempt, out);
} catch {
IERC20(tokenIn).forceApprove(address(swapRouter), 0);
attempt /= 2;
}
}
}
// ---------------------------------------------------------------------
// Reward ledger
// ---------------------------------------------------------------------
function setShare(address account, uint256 balance) external onlyToken {
if (excludedFromDividends[account]) return;
uint256 newShare = balance >= minimumBalanceForDividends ? balance : 0;
uint256 oldShare = shares[account];
if (newShare == oldShare) return;
if (newShare > oldShare) {
uint256 delta = newShare - oldShare;
magnifiedDividendCorrections[account] -= int256(magnifiedDividendPerShare * delta);
totalShares += delta;
if (oldShare == 0) _addHolder(account);
} else {
uint256 delta = oldShare - newShare;
magnifiedDividendCorrections[account] += int256(magnifiedDividendPerShare * delta);
totalShares -= delta;
if (newShare == 0) _removeHolder(account);
}
shares[account] = newShare;
}
function claim() external nonReentrant returns (uint256) {
return _withdrawDividend(msg.sender);
}
function processAccount(address account) external nonReentrant returns (uint256) {
return _withdrawDividend(account);
}
function process(uint256 gasLimit) external nonReentrant returns (uint256 iterations, uint256 claims) {
uint256 numHolders = holders.length;
if (numHolders == 0) return (0, 0);
uint256 gasUsed = 0;
uint256 gasLeftStart = gasleft();
uint256 index = lastProcessedIndex;
while (gasUsed < gasLimit && iterations < numHolders) {
if (index >= holders.length) index = 0;
if (_withdrawDividend(holders[index]) > 0) claims++;
index++;
iterations++;
gasUsed = gasLeftStart - gasleft();
}
lastProcessedIndex = index >= holders.length ? 0 : index;
}
function withdrawableDividendOf(address account) public view returns (uint256) {
return accumulativeDividendOf(account) - withdrawnDividends[account];
}
function accumulativeDividendOf(address account) public view returns (uint256) {
return uint256(
int256(magnifiedDividendPerShare * shares[account]) + magnifiedDividendCorrections[account]
) / MAGNITUDE;
}
function pendingDistribution() public view returns (uint256) {
uint256 d = totalDividendsDistributed;
uint256 w = totalDividendsWithdrawn;
return d > w ? d - w : 0;
}
function holderCount() external view returns (uint256) {
return holders.length;
}
function _withdrawDividend(address account) private returns (uint256) {
uint256 withdrawable = withdrawableDividendOf(account);
if (withdrawable == 0) return 0;
uint256 available = IERC20(rewardToken).balanceOf(address(this));
if (available == 0) return 0;
if (withdrawable > available) withdrawable = available;
withdrawnDividends[account] += withdrawable;
totalDividendsWithdrawn += withdrawable;
(bool ok, bytes memory data) = rewardToken.call(
abi.encodeWithSelector(IERC20.transfer.selector, account, withdrawable)
);
if (!ok || (data.length > 0 && !abi.decode(data, (bool)))) {
withdrawnDividends[account] -= withdrawable;
totalDividendsWithdrawn -= withdrawable;
emit DividendWithdrawFailed(account, withdrawable);
return 0;
}
emit DividendWithdrawn(account, withdrawable);
return withdrawable;
}
function _addHolder(address account) private {
if (holderIndexPlusOne[account] != 0) return;
holders.push(account);
holderIndexPlusOne[account] = holders.length;
}
function _removeHolder(address account) private {
uint256 indexPlusOne = holderIndexPlusOne[account];
if (indexPlusOne == 0) return;
uint256 index = indexPlusOne - 1;
uint256 lastIndex = holders.length - 1;
if (index != lastIndex) {
address lastHolder = holders[lastIndex];
holders[index] = lastHolder;
holderIndexPlusOne[lastHolder] = indexPlusOne;
}
holders.pop();
holderIndexPlusOne[account] = 0;
}
}
Chain explorer3894msChain node80ms