// SPDX-License-Identifier: MIT pragma solidity 0.8.38; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; interface IGiggleRouter { function WETH() external view returns (address); function factory() external view returns (address); function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256,uint256,address[] calldata,address,uint256) external; } interface IGiggleFactory { function getPair(address,address) external view returns (address); function createPair(address,address) external returns (address); } interface IGigglePair is IERC20 { function token0() external view returns (address); function token1() external view returns (address); function getReserves() external view returns (uint112,uint112,uint32); function mint(address) external returns (uint256); function burn(address) external returns (uint256,uint256); function skim(address) external; } interface IGiggleWrapped is IERC20 { function deposit() external payable; } /// @notice New deployment only. UUPS authority remains with the current owner. /// @dev Native BNB rewards; DEAD transfers do not reduce totalSupply. contract GiggleAgent is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, IERC20 { string public constant name = "giggle"; string public constant symbol = "giggle"; uint8 public constant decimals = 0; uint256 public constant totalSupply = 8_888_800_000_000; uint256 public constant MAX_LOTS = 10_000; uint256 public constant MAX_PER_TX = 10; uint256 public constant BASE_PRICE = 0.003 ether; uint256 public constant TOKENS_PER_LOT = 555_550_000; uint256 public constant MINT_ALLOCATION = MAX_LOTS * TOKENS_PER_LOT; uint256 public constant BOOTSTRAP_ALLOCATION = totalSupply - MINT_ALLOCATION; uint256 public constant MINT_LP_BPS = 6_000; uint256 public constant TAX_BPS = 200; uint256 public constant donationBps = 1_000; uint256 public constant dividendBps = 3_000; uint256 public constant liquidityBps = 500; uint256 public constant burnBps = 500; uint256 public constant marketingBps = 5_000; address public constant DEAD = 0x000000000000000000000000000000000000dEaD; address public constant EXPECTED_TREASURY = 0xff615BD223EAf7a5052aeB5f4c4B2D0e33FF3eA5; address public constant EXPECTED_DONATION = 0xC7f501D25Ea088aeFCa8B4b3ebD936aAe12bF4A4; address public constant OFFICIAL_AGENT_ID_REGISTRY = 0x60B9262456dE011F7d15a54CdD8c48bAaa8B022F; bytes32 public constant MINT_AUTHORIZATION_TYPEHASH = keccak256("MintAuthorization(address wallet,uint256 id,uint256 lots,uint256 nonce,uint256 deadline)"); uint256 private constant INDEX_SCALE = 1e36; uint256 private constant AUTO_GAS_CAP = 900_000; uint256 private constant AUTO_OPEN_GAS_CAP = 2_000_000; IERC721 public identity; IGiggleRouter public router; IGigglePair public pair; IGiggleWrapped public wrapped; address public TREASURY; address public DONATION; address public attestor; bool private tokenFirst; bool private busy; bool public opened; uint256 public totalMinted; uint256 public mintLiquidityBnb; uint256 public treasuryOwed; uint256 public donationOwed; mapping(address => uint256) public mintNonces; mapping(address => uint256) public mintedByWallet; mapping(address => uint256) public walletId; mapping(uint256 => address) public idWallet; mapping(address => uint256) private balances; mapping(address => mapping(address => uint256)) public allowance; mapping(address => bool) public isExcludedFromReward; uint256 public eligibleSupply; uint256 public rewardPerToken; uint256 private indexRemainder; mapping(address => uint256) private lastRewardIndex; mapping(address => uint256) private earned; mapping(address => uint256) private fractions; uint256 public dividendLiability; uint256 public unallocatedDividendBnb; uint256 public totalDividendsFunded; uint256 public totalDividendsPaid; uint256 public totalDonationPaid; uint256 public totalTaxTokens; uint256 public pendingDonationTokens; uint256 public pendingDividendTokens; uint256 public pendingLiquidityTokens; uint256 public pendingMarketingTokens; uint256 public taxLiquidityPairTokens; uint256 public taxLiquidityWbnb; uint256 private lpHalfRemainder; uint256 public minSettlementTokens; uint256 public maxSettlementTokens; address[] private rewardHolders; mapping(address => uint256) private holderPosition; uint256 public rewardCursor; // Opening proceeds are kept separate from post-launch tax balances. uint256 private reservedLaunchSlot; uint256 public mintTreasuryBnb; bool private reservedLaunchFlag; event LotsMinted(address indexed wallet,uint256 indexed id,uint256 lots,uint256 tokens,uint256 cost,uint256 nonce); event LiquidityOpened(uint256 tokens,uint256 bnb,uint256 lpBurned); event TaxAssessed(address indexed from,address indexed to,uint256 donation,uint256 dividend,uint256 lp,uint256 burn,uint256 marketing); event TaxesSettled(uint256 tokensSold,uint256 bnbReceived,uint256 donationBnb,uint256 dividendBnb,uint256 liquidityBnb,uint256 marketingBnb); event RewardsFunded(uint256 amount,uint256 eligibleSupply); event RewardClaimed(address indexed holder,uint256 amount); event PaymentDeferred(address indexed recipient,uint256 amount); event PaymentSent(address indexed recipient,uint256 amount); event SettlementDeferred(); event LiquidityDeferred(); event TaxLpPaired(uint256 tokens,uint256 wbnb,uint256 lpBurned); event RewardPaymentDeferred(address indexed holder); event AttestorUpdated(address indexed previous,address indexed current); event SettlementRangeUpdated(uint256 minimum,uint256 maximum); event AutoOpenDeferred(); event UnsoldAllocationSentToDead(uint256 mintTokens,uint256 liquidityTokens); event OpeningDustRecovered(uint256 wbnbReserved,uint256 tokenReserveBeforeFinalMint,uint256 wbnbReserveBeforeFinalMint); error InvalidInput(); error InvalidIdentity(); error InvalidAuthorization(); error QuotaExceeded(); error IncorrectPayment(); error Expired(); error Reentered(); error TradingClosed(); error PoolNotReady(); error NothingToSettle(); error InternalOnly(); error BnbPaymentFailed(); error InsufficientSettlementGas(); modifier guarded() { if (busy) revert Reentered(); busy = true; _; busy = false; } modifier beforeDeadline(uint256 deadline) { if (block.timestamp > deadline) revert Expired(); _; } /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } function initialize(address initialOwner,address registry,address router_,address treasury_,address donation_,address attestor_) external initializer { if (initialOwner == address(0) || registry.code.length == 0 || router_.code.length == 0 || treasury_ != EXPECTED_TREASURY || donation_ != EXPECTED_DONATION || treasury_ == DEAD || donation_ == DEAD || treasury_ == address(this) || donation_ == address(this) || treasury_ == registry || donation_ == registry || treasury_ == router_ || donation_ == router_) revert InvalidInput(); if (block.chainid == 56 && registry != OFFICIAL_AGENT_ID_REGISTRY) revert InvalidInput(); __Ownable_init(initialOwner); __Ownable2Step_init(); identity = IERC721(registry); router = IGiggleRouter(router_); wrapped = IGiggleWrapped(router.WETH()); IGiggleFactory factory = IGiggleFactory(router.factory()); if (address(wrapped).code.length == 0 || address(factory).code.length == 0) revert InvalidInput(); address market = factory.getPair(address(this),address(wrapped)); if (market == address(0)) market = factory.createPair(address(this),address(wrapped)); if (treasury_ == market || donation_ == market || treasury_ == address(wrapped) || donation_ == address(wrapped)) revert InvalidInput(); pair = IGigglePair(market); tokenFirst = pair.token0() == address(this); if ((tokenFirst ? pair.token1() : pair.token0()) != address(wrapped) || (!tokenFirst && pair.token1() != address(this))) revert InvalidInput(); TREASURY = treasury_; DONATION = donation_; attestor = attestor_; isExcludedFromReward[address(0)] = true; isExcludedFromReward[address(this)] = true; isExcludedFromReward[market] = true; isExcludedFromReward[router_] = true; isExcludedFromReward[address(wrapped)] = true; isExcludedFromReward[registry] = true; isExcludedFromReward[treasury_] = true; isExcludedFromReward[donation_] = true; isExcludedFromReward[DEAD] = true; minSettlementTokens = totalSupply / 100_000; maxSettlementTokens = totalSupply / 20_000; balances[address(this)] = totalSupply; emit Transfer(address(0),address(this),totalSupply); } function _authorizeUpgrade(address) internal override onlyOwner {} function DOMAIN_SEPARATOR() public view returns (bytes32) { return keccak256(abi.encode(keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("GiggleAgentMint"),keccak256("1"),block.chainid,address(this))); } function version() external pure virtual returns (uint256) { return 1; } receive() external payable { if (msg.sender != address(router) && msg.sender != address(wrapped)) revert InvalidInput(); } function setAttestor(address next) external onlyOwner guarded { emit AttestorUpdated(attestor,next); attestor = next; } function setSettlementRange(uint256 minimum,uint256 maximum) external onlyOwner guarded { if (minimum == 0 || minimum > maximum || maximum > totalSupply / 100) revert InvalidInput(); minSettlementTokens = minimum; maxSettlementTokens = maximum; emit SettlementRangeUpdated(minimum,maximum); } function mintOpen() external view returns (bool) { return !opened && totalMinted < MAX_LOTS; } function previewMint(uint256 lots) public view returns (uint256 cost,uint256 tokens,uint256 liquidityBnb,uint256 treasuryBnb) { if (opened || lots == 0 || lots > MAX_PER_TX || totalMinted + lots > MAX_LOTS) revert QuotaExceeded(); cost = lots * BASE_PRICE; tokens = lots * TOKENS_PER_LOT; liquidityBnb = cost * MINT_LP_BPS / 10_000; treasuryBnb = cost - liquidityBnb; } function mint(uint256 id,uint256 lots,uint256 deadline,bytes calldata signature) external payable guarded beforeDeadline(deadline) { if (id == 0 || identity.ownerOf(id) != msg.sender || (walletId[msg.sender] != 0 && walletId[msg.sender] != id) || (idWallet[id] != address(0) && idWallet[id] != msg.sender)) revert InvalidIdentity(); uint256 nonce = mintNonces[msg.sender]; if (attestor == address(0)) { // The official soulbound Agent ID is itself the default wallet gate. if (signature.length != 0) revert InvalidAuthorization(); } else { bytes32 digest = keccak256(abi.encodePacked(hex"1901",DOMAIN_SEPARATOR(),keccak256(abi.encode(MINT_AUTHORIZATION_TYPEHASH,msg.sender,id,lots,nonce,deadline)))); if (ECDSA.recover(digest,signature) != attestor) revert InvalidAuthorization(); } (uint256 cost,uint256 tokens,uint256 lp,uint256 treasury) = previewMint(lots); if (msg.value != cost) revert IncorrectPayment(); mintNonces[msg.sender] = nonce + 1; walletId[msg.sender] = id; idWallet[id] = msg.sender; mintedByWallet[msg.sender] += lots; totalMinted += lots; mintLiquidityBnb += lp; mintTreasuryBnb += treasury; _move(address(this),msg.sender,tokens); emit LotsMinted(msg.sender,id,lots,tokens,cost,nonce); if (totalMinted == MAX_LOTS) _attemptAutoOpen(); } function balanceOf(address account) public view returns (uint256) { return balances[account]; } function approve(address spender,uint256 amount) external returns (bool) { if (spender == address(0)) revert InvalidInput(); allowance[msg.sender][spender] = amount; emit Approval(msg.sender,spender,amount); return true; } function transfer(address to,uint256 amount) external returns (bool) { if (busy && msg.sender != address(router) && !(!opened && msg.sender == address(pair) && to == address(this))) revert Reentered(); _move(msg.sender,to,amount); return true; } function transferFrom(address from,address to,uint256 amount) external returns (bool) { if (busy && msg.sender != address(router)) revert Reentered(); uint256 allowed = allowance[from][msg.sender]; if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; _move(from,to,amount); return true; } function _delta(address account) private view returns (uint256 whole,uint256 fraction) { uint256 delta = rewardPerToken - lastRewardIndex[account]; fraction = mulmod(balances[account],delta,INDEX_SCALE) + fractions[account]; whole = Math.mulDiv(balances[account],delta,INDEX_SCALE) + fraction / INDEX_SCALE; fraction %= INDEX_SCALE; } function claimable(address holder) public view returns (uint256) { if (isExcludedFromReward[holder]) return 0; (uint256 added,) = _delta(holder); return earned[holder] + added; } function _checkpoint(address account) private { (uint256 added,uint256 fraction) = _delta(account); earned[account] += added; fractions[account] = fraction; lastRewardIndex[account] = rewardPerToken; } function _debit(address account,uint256 amount) private { if (!isExcludedFromReward[account]) { _checkpoint(account); eligibleSupply -= amount; } balances[account] -= amount; if (balances[account] == 0 && earned[account] == 0) _removeHolder(account); } function _credit(address account,uint256 amount) private { if (!isExcludedFromReward[account]) { _checkpoint(account); eligibleSupply += amount; if (amount != 0 && holderPosition[account] == 0) { rewardHolders.push(account); holderPosition[account] = rewardHolders.length; } } balances[account] += amount; } function _removeHolder(address holder) private { uint256 pos = holderPosition[holder]; if (pos == 0) return; address last = rewardHolders[rewardHolders.length - 1]; rewardHolders[pos - 1] = last; holderPosition[last] = pos; rewardHolders.pop(); delete holderPosition[holder]; if (rewardCursor >= rewardHolders.length) rewardCursor = 0; } function _move(address from,address to,uint256 amount) private { if (from == address(0) || to == address(0)) revert InvalidInput(); if (amount == 0) { emit Transfer(from,to,0); return; } bool market = from == address(pair) || to == address(pair); if (market && !opened && from != address(this) && !(busy && from == address(pair) && to == address(this))) revert TradingClosed(); _debit(from,amount); uint256 tax = market && opened && from != address(this) ? amount * TAX_BPS / 10_000 : 0; if (tax != 0) { uint256 donation = tax * donationBps / 10_000; uint256 dividend = tax * dividendBps / 10_000; uint256 lp = tax * liquidityBps / 10_000; uint256 burn = tax * burnBps / 10_000; uint256 marketing = tax - donation - dividend - lp - burn; pendingDonationTokens += donation; pendingDividendTokens += dividend; pendingLiquidityTokens += lp; pendingMarketingTokens += marketing; totalTaxTokens += tax; _credit(DEAD,burn); _credit(address(this),tax-burn); emit Transfer(from,DEAD,burn); emit Transfer(from,address(this),tax-burn); emit TaxAssessed(from,to,donation,dividend,lp,burn,marketing); } bool sell = to == address(pair) && from != address(this) && from != to && !busy && opened; uint256 net = amount - tax; // Hold the outer seller's input until the nested tax swap finishes. _credit(sell ? address(this) : to,net); if (sell) { if (_pendingTokens() >= minSettlementTokens) _attemptSettlement(); balances[address(this)] -= net; balances[to] += net; } emit Transfer(from,to,net); if (market && opened && !busy && rewardPerToken != 0) { busy = true; _processRewards(2); busy = false; } } function _pendingTokens() private view returns (uint256) { return pendingDonationTokens + pendingDividendTokens + pendingLiquidityTokens + pendingMarketingTokens; } function _attemptSettlement() private { uint256 remaining = gasleft(); if (remaining < 65_000) revert InsufficientSettlementGas(); uint256 forwarded = Math.min(AUTO_GAS_CAP,remaining-55_000); (bool ok,) = address(this).call{gas:forwarded}(abi.encodeCall(this.settleTaxes,(maxSettlementTokens,0,block.timestamp))); // Do not let estimation choose a cheap failing branch that skips tax work. if (!ok && forwarded < AUTO_GAS_CAP) revert InsufficientSettlementGas(); if (!ok) emit SettlementDeferred(); } /// @notice DEV may launch before sellout. After sellout anyone may retry a deferred launch. function openLiquidity(uint256 deadline) external guarded beforeDeadline(deadline) { if (totalMinted < MAX_LOTS) _checkOwner(); _openLiquidity(); } function openFromMint() external { if (msg.sender != address(this) || !busy || totalMinted != MAX_LOTS) revert InternalOnly(); _openLiquidity(); } function _attemptAutoOpen() private { uint256 remaining = gasleft(); if (remaining < 65_000) revert InsufficientSettlementGas(); uint256 forwarded = Math.min(AUTO_OPEN_GAS_CAP,remaining-55_000); (bool ok,) = address(this).call{gas:forwarded}(abi.encodeCall(this.openFromMint,())); // A cheap gas estimate must not silently skip an otherwise successful opening. if (!ok && forwarded < AUTO_OPEN_GAS_CAP) revert InsufficientSettlementGas(); if (!ok) emit AutoOpenDeferred(); } function _openLiquidity() private { uint256 lots = totalMinted; if (opened || lots == 0 || pair.totalSupply() != 0) revert PoolNotReady(); uint256 bnb = lots * BASE_PRICE * MINT_LP_BPS / 10_000; uint256 tokens = lots * TOKENS_PER_LOT * MINT_LP_BPS / 10_000; uint256 unsold = (MAX_LOTS-lots)*TOKENS_PER_LOT; uint256 unusedLp = BOOTSTRAP_ALLOCATION-tokens; if (mintLiquidityBnb != bnb || mintTreasuryBnb != lots*BASE_PRICE-bnb) revert PoolNotReady(); wrapped.deposit{value:bnb}(); (uint256 t,uint256 w) = _normalizeEmptyPair(bnb,BOOTSTRAP_ALLOCATION+unsold); if (t >= tokens || w >= bnb) revert PoolNotReady(); _move(address(this),address(pair),tokens-t); if (!wrapped.transfer(address(pair),bnb-w)) revert PoolNotReady(); uint256 minted = pair.mint(DEAD); (uint256 actualTokens,uint256 actualWbnb) = _reserves(); if (minted == 0 || actualTokens != tokens || actualWbnb != bnb) revert PoolNotReady(); uint256 recovered = wrapped.balanceOf(address(this)); if (recovered != 0 || t != 0 || w != 0) { taxLiquidityWbnb += recovered; emit OpeningDustRecovered(recovered,t,w); } if (unsold+unusedLp != 0) { _move(address(this),DEAD,unsold+unusedLp); emit UnsoldAllocationSentToDead(unsold,unusedLp); } opened = true; mintLiquidityBnb = 0; uint256 marketing = mintTreasuryBnb; mintTreasuryBnb = 0; _payDestination(TREASURY,marketing); emit LiquidityOpened(tokens,bnb,minted); } function _normalizeEmptyPair(uint256 bootstrapBnb,uint256 availableTokens) private returns (uint256 t,uint256 w) { (t,w) = _reserves(); // Before launch no outside address can fund the Giggle side of the pair. if (t != 0 || balances[address(pair)] != 0) revert PoolNotReady(); if (wrapped.balanceOf(address(pair)) > w) pair.skim(address(this)); if (w != 0) { // Synced WBNB cannot be skimmed. A provisional LP minted to this contract // and immediately burned recovers it except for permanent minimum liquidity. _move(address(this),address(pair),availableTokens); if (!wrapped.transfer(address(pair),bootstrapBnb)) revert PoolNotReady(); uint256 provisional = pair.mint(address(this)); if (provisional == 0 || !pair.transfer(address(pair),provisional)) revert PoolNotReady(); pair.burn(address(this)); (t,w) = _reserves(); } } function _reserves() private view returns (uint256 t,uint256 w) { (uint112 a,uint112 b,) = pair.getReserves(); return tokenFirst ? (a,b) : (b,a); } function _checkPool() private view { (uint256 t,uint256 w) = _reserves(); if (!opened || t == 0 || w == 0 || balances[address(pair)] != t || wrapped.balanceOf(address(pair)) != w) revert PoolNotReady(); } function settleTaxes(uint256 maxTokens,uint256 minBnbOut,uint256 deadline) external guarded beforeDeadline(deadline) { if (msg.sender != address(this)) _checkOwner(); _checkPool(); uint256 pending = _pendingTokens(); uint256 batch = Math.min(pending,Math.min(maxTokens,maxSettlementTokens)); if (batch == 0) revert NothingToSettle(); uint256 d = Math.mulDiv(pendingDonationTokens,batch,pending); uint256 r = Math.mulDiv(pendingDividendTokens,batch,pending); uint256 l = Math.mulDiv(pendingLiquidityTokens,batch,pending); uint256 m = Math.mulDiv(pendingMarketingTokens,batch,pending); if (d+r+l+m == 0) revert NothingToSettle(); uint256 lpSale = (l + lpHalfRemainder) / 2; lpHalfRemainder = (l + lpHalfRemainder) % 2; uint256 sold = d+r+m+lpSale; if (sold == 0) revert NothingToSettle(); pendingDonationTokens -= d; pendingDividendTokens -= r; pendingLiquidityTokens -= l; pendingMarketingTokens -= m; taxLiquidityPairTokens += l-lpSale; address[] memory route = new address[](2); route[0] = address(this); route[1] = address(wrapped); uint256 beforeBnb = address(this).balance; allowance[address(this)][address(router)] = sold; emit Approval(address(this),address(router),sold); router.swapExactTokensForETHSupportingFeeOnTransferTokens(sold,minBnbOut,route,address(this),deadline); allowance[address(this)][address(router)] = 0; uint256 received = address(this).balance-beforeBnb; if (received == 0 || received < minBnbOut) revert PoolNotReady(); uint256 donationBnb = Math.mulDiv(received,d,sold); uint256 dividendBnb = Math.mulDiv(received,r,sold); uint256 liquidityBnb = Math.mulDiv(received,lpSale,sold); uint256 marketingBnb = received-donationBnb-dividendBnb-liquidityBnb; if (liquidityBnb != 0) { wrapped.deposit{value:liquidityBnb}(); taxLiquidityWbnb += liquidityBnb; } _fundRewards(dividendBnb); _payDestination(DONATION,donationBnb); _payDestination(TREASURY,marketingBnb); if (taxLiquidityPairTokens != 0 && taxLiquidityWbnb != 0 && gasleft() > 230_000) { (bool ok,) = address(this).call{gas:200_000}(abi.encodeCall(this.pairFromSettlement,())); if (!ok) emit LiquidityDeferred(); } emit TaxesSettled(sold,received,donationBnb,dividendBnb,liquidityBnb,marketingBnb); } function pairFromSettlement() external returns (uint256,uint256,uint256) { if (msg.sender != address(this) || !busy) revert InternalOnly(); return _pairReservedLiquidity(); } function pairTaxLiquidity(uint256 deadline) external onlyOwner guarded beforeDeadline(deadline) { _checkPool(); (,,uint256 minted) = _pairReservedLiquidity(); if (minted == 0) revert NothingToSettle(); } function _pairReservedLiquidity() private returns (uint256 addTokens,uint256 addWbnb,uint256 minted) { (uint256 t,uint256 w) = _reserves(); addTokens = Math.min(taxLiquidityPairTokens,Math.mulDiv(taxLiquidityWbnb,t,w)); addWbnb = Math.mulDiv(addTokens,w,t,Math.Rounding.Ceil); if (addTokens == 0 || addWbnb == 0 || Math.mulDiv(addTokens,pair.totalSupply(),t) == 0 || Math.mulDiv(addWbnb,pair.totalSupply(),w) == 0) return (0,0,0); taxLiquidityPairTokens -= addTokens; taxLiquidityWbnb -= addWbnb; _move(address(this),address(pair),addTokens); if (!wrapped.transfer(address(pair),addWbnb)) revert PoolNotReady(); minted = pair.mint(DEAD); emit TaxLpPaired(addTokens,addWbnb,minted); } function _fundRewards(uint256 amount) private { unallocatedDividendBnb += amount; if (eligibleSupply == 0 || unallocatedDividendBnb == 0) return; amount = unallocatedDividendBnb; unallocatedDividendBnb = 0; uint256 remainder = mulmod(amount,INDEX_SCALE,eligibleSupply) + indexRemainder; rewardPerToken += Math.mulDiv(amount,INDEX_SCALE,eligibleSupply) + remainder / eligibleSupply; indexRemainder = remainder % eligibleSupply; dividendLiability += amount; totalDividendsFunded += amount; emit RewardsFunded(amount,eligibleSupply); } function distributePendingDividends() external onlyOwner guarded { _fundRewards(0); } function claimRewardsFor(address holder) external guarded { if (holder == address(0) || isExcludedFromReward[holder]) revert InvalidInput(); if (!_payReward(holder)) revert NothingToSettle(); } function _payReward(address holder) private returns (bool) { _checkpoint(holder); uint256 amount = earned[holder]; if (amount == 0) return false; earned[holder] = 0; dividendLiability -= amount; totalDividendsPaid += amount; if (balances[holder] == 0) _removeHolder(holder); (bool ok,) = holder.call{value:amount,gas:30_000}(""); if (!ok) revert BnbPaymentFailed(); emit RewardClaimed(holder,amount); return true; } function rewardHolderCount() external view returns (uint256) { return rewardHolders.length; } function processRewards(uint256 maxAccounts) external onlyOwner guarded { if (maxAccounts == 0 || maxAccounts > 16) revert InvalidInput(); _processRewards(maxAccounts); } function payFromQueue(address holder) external { if (msg.sender != address(this) || !busy) revert InternalOnly(); _payReward(holder); } function _processRewards(uint256 maxAccounts) private { uint256 visits = Math.min(maxAccounts,rewardHolders.length); for (uint256 i; i < visits && rewardHolders.length != 0 && gasleft() > 110_000; ++i) { address holder = rewardHolders[rewardCursor]; rewardCursor = (rewardCursor+1) % rewardHolders.length; if (claimable(holder) != 0) { (bool ok,) = address(this).call{gas:90_000}(abi.encodeCall(this.payFromQueue,(holder))); if (!ok) emit RewardPaymentDeferred(holder); } } } function _payDestination(address destination,uint256 amount) private { if (amount == 0) return; (bool ok,) = destination.call{value:amount,gas:30_000}(""); if (ok) { if (destination == DONATION) totalDonationPaid += amount; emit PaymentSent(destination,amount); } else { if (destination == DONATION) donationOwed += amount; else treasuryOwed += amount; emit PaymentDeferred(destination,amount); } } /// @notice Anyone can retry; beneficiary and booked amount cannot be changed. function retryDestination(bool donation) external guarded { uint256 amount = donation ? donationOwed : treasuryOwed; if (amount == 0) revert NothingToSettle(); if (donation) donationOwed = 0; else treasuryOwed = 0; address recipient = donation ? DONATION : TREASURY; (bool ok,) = recipient.call{value:amount,gas:30_000}(""); if (!ok) revert BnbPaymentFailed(); if (donation) totalDonationPaid += amount; emit PaymentSent(recipient,amount); } }