FLOWv4
0x856e1c2db4ae8b31f698fb052432a560ec0407f7
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,738 bytes
ABI entries
84
40 read · 23 write
License
none
Contract information
- Address
- 0x856e1c2db4ae8b31f698fb052432a560ec0407f7
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x5c03d8Fe39…e89ee2A61b
- Creation tx
- 0xe73daa06f6…7ba646873e
Token
- Name
- FLOW Backwards
- Symbol
- WOLF
- Decimals
- 18
- Holders
- 19
- Market
- View token page →
Read contract (40)
BPS() → uint256
BUYBACK_MAX_SLIP_BPS() → uint256
FORFEIT_LP_BPS() → uint256
FORFEIT_PROJECT_BPS() → uint256
POOL_FEE() → uint24
POOL_TICK_SPACING() → int24
allowance(address, address) → uint256
balanceOf(address) → uint256
basket(address) → uint256
basketOf(address) → uint256
burned() → uint256
curve() → address
decimals() → uint8
graduated() → bool
holdDuration(address) → uint256
holdSince(address) → uint256
hook() → address
initialSupply() → uint256
isAMMPair(address) → bool
isExcludedFromFees(address) → bool
isExcludedFromLimits(address) → bool
isExcludedFromRewards(address) → bool
magnifiedRewardPerShare() → uint256
maxTx() → uint256
maxWallet() → uint256
name() → string
owner() → address
pendingLiquidity() → uint256
pendingProject() → uint256
platformVault() → address
poolManager() → address
positionManager() → address
restricted() → bool
rewardCheckpoint(address) → uint256
symbol() → string
totalShares() → uint256
totalSupply() → uint256
tradingEnabled() → bool
treasury() → address
universalRouter() → address
Events (9)
ApprovalBasketFilledBasketMovedForfeitedHookSetOwnershipTransferredRedeemedTradingEnabledTransfer
ABI
[
{
"inputs": [
{
"internalType": "string",
"name": "name_",
"type": "string"
},
{
"internalType": "string",
"name": "symbol_",
"type": "string"
},
{
"internalType": "address",
"name": "treasury_",
"type": "address"
},
{
"internalType": "uint256",
"name": "supply_",
"type": "uint256"
},
{
"internalType": "address",
"name": "poolManager_",
"type": "address"
},
{
"internalType": "address",
"name": "positionManager_",
"type": "address"
},
{
"internalType": "address",
"name": "universalRouter_",
"type": "address"
},
{
"internalType": "bool",
"name": "restricted_",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "allowance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientAllowance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
},
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "ERC20InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "approver",
"type": "address"
}
],
"name": "ERC20InvalidApprover",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "receiver",
"type": "address"
}
],
"name": "ERC20InvalidReceiver",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "sender",
"type": "address"
}
],
"name": "ERC20InvalidSender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "ERC20InvalidSpender",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "nativeAdded",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "newPerShare",
"type": "uint256"
}
],
"name": "BasketFilled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "BasketMoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "who",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "toProject",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "toLp",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "toHolders",
"type": "uint256"
}
],
"name": "Forfeited",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "hook",
"type": "address"
}
],
"name": "HookSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "who",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokensBurned",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "nativePaid",
"type": "uint256"
}
],
"name": "Redeemed",
"type": "event"
},
{
"anonymous": false,
"inputs": [],
"name": "TradingEnabled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"inputs": [],
"name": "BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "BUYBACK_MAX_SLIP_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FORFEIT_LP_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "FORFEIT_PROJECT_BPS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "basket",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "basketOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "burned",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "curve",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "enableTrading",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "fundBaskets",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "graduated",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "holdDuration",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "holdSince",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "hook",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "initialSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isAMMPair",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isExcludedFromFees",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isExcludedFromLimits",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "isExcludedFromRewards",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "magnifiedRewardPerShare",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxTx",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "maxWallet",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "onGraduate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingLiquidity",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingProject",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformVault",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "poolManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionManager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "processLiquidity",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "processProject",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "n",
"type": "uint256"
}
],
"name": "redeem",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "redeemAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "removeLimits",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "restricted",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "rewardCheckpoint",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "p",
"type": "address"
},
{
"internalType": "bool",
"name": "v",
"type": "bool"
}
],
"name": "setAMMPair",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "c",
"type": "address"
}
],
"name": "setCurve",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "a",
"type": "address"
},
{
"internalType": "bool",
"name": "v",
"type": "bool"
}
],
"name": "setExcludedFromFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "a",
"type": "address"
},
{
"internalType": "bool",
"name": "v",
"type": "bool"
}
],
"name": "setExcludedFromLimits",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "a",
"type": "address"
},
{
"internalType": "bool",
"name": "v",
"type": "bool"
}
],
"name": "setExcludedFromRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "h",
"type": "address"
}
],
"name": "setHook",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "maxTx_",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "maxWallet_",
"type": "uint256"
}
],
"name": "setLimits",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "v",
"type": "address"
}
],
"name": "setPlatformVault",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "t",
"type": "address"
}
],
"name": "setTreasury",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalShares",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "tradingEnabled",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "treasury",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "universalRouter",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "unlockCallback",
"outputs": [
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
// ^0.8.24 (not fixed 0.8.26): this token uses no V4 features, so it compiles under BOTH
// Hardhat (0.8.24 — lets us unit-test it now) and Foundry (0.8.26 — for the hook stack later).
pragma solidity ^0.8.24;
/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FLOW (V4 route) — earn while you hold.
┌─────────────────────────────────────────────────────────────────────────┐
│ ⚠️ WORK IN PROGRESS — V4 migration. NOT AUDITED. This is the "plain │
│ ERC-20" token for the Uniswap-V4-hook launch: the 5% trade tax lives │
│ in FlowHook.sol (step 1), NOT in this token. See │
│ docs/V4-HOOK-MIGRATION.md. Named `FLOWv4` during the migration so it │
│ doesn't collide with the audited V2 `FLOW`; rename to `FLOW` on cut-over.│
└─────────────────────────────────────────────────────────────────────────┘
What CHANGED vs the V2 token:
• NO fee-on-transfer tax in _update. On V4 the tax is a swap-fee taken by the
hook (in ETH) and forwarded here via fundBaskets() — same accumulator, same
baskets. Removed: _swapBack / manualSwapBack / autoSwap / swapThreshold /
maxSwapBack / inSwap, and the V2 router+pair plumbing.
• Exclusions retarget from the V2 `pair` to the V4 singletons
(PoolManager / PositionManager / UniversalRouter): they hold FLOW transiently
during swaps/liquidity and must not accrue baskets or forfeit.
What STAYED the same (economics unchanged):
• Baskets: native (ETH) streamed pro-rata by balance via the O(1) accumulator.
• Forfeit: any outgoing move (sell OR wallet transfer) forfeits a PROPORTIONAL
slice — 20% project · 40% buyback+burn · 40% holders. Touches ONLY basket
accounting, not token amounts, so it works fine on a plain ERC-20.
• redeem(n): burn n FLOW → withdraw n/balance of your basket (native), CEI + guard.
• Bonding-curve phase is IDENTICAL: the curve charges its 5% in native and calls
fundBaskets() (no DEX involved). V4 only appears at graduation (LaunchCurve).
• holdSince[wallet]: display-only hold-time.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import {PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
import {CurrencySettler} from "@openzeppelin/uniswap-hooks/src/utils/CurrencySettler.sol";
interface ICurve {
function token() external view returns (address);
}
contract FLOWv4 is ERC20, Ownable, ReentrancyGuard, IUnlockCallback {
using CurrencySettler for Currency;
using BalanceDeltaLibrary for BalanceDelta;
using StateLibrary for IPoolManager;
using PoolIdLibrary for PoolKey;
// ───────────────────────────── constants ─────────────────────────────
uint256 private constant MAGNITUDE = 2 ** 128; // fixed-point scalar for the accumulator
uint256 public constant BPS = 10_000;
uint24 public constant POOL_FEE = 3000; // must match LaunchCurveV4.LP_FEE
int24 public constant POOL_TICK_SPACING = 60; // must match LaunchCurveV4.TICK_SPACING
// forfeit split (of a forfeited basket): 20% project / 40% buyback / remainder(40%) holders.
// (No cross-token "platform" slice — FLOW earns from the ecosystem via the hook's 0.25% trade cut.)
uint256 public constant FORFEIT_PROJECT_BPS = 2_000; // 20% project treasury
uint256 public constant FORFEIT_LP_BPS = 4_000; // 40% buyback + burn
// remainder (40%) spills to other holders
// ───────────────────────────── reward accounting ─────────────────────
// Basket = native token accrued to a holder. MasterChef-style accumulator:
// `magnifiedRewardPerShare` only moves when native is added; each account
// crystallises its pending accrual into `basket[account]` (via _settle) before
// any balance change.
uint256 public magnifiedRewardPerShare;
mapping(address => uint256) public rewardCheckpoint; // last seen magnifiedRewardPerShare
mapping(address => uint256) public basket; // crystallised native owed
mapping(address => uint256) private _shares; // mirror of eligible balance
uint256 public totalShares; // Σ eligible balances
mapping(address => bool) public isExcludedFromRewards;
// ───────────────────────────── forfeit reserves ──────────────────────
uint256 public pendingProject; // native queued for the project treasury
uint256 public pendingLiquidity; // native queued for buyback + burn
// ───────────────────────────── hold-time (display only) ──────────────
mapping(address => uint256) public holdSince; // weighted-avg acquire time; reset on any move-out
// ───────────────────────────── trading config ────────────────────────
address public treasury;
// ── V4 infrastructure (singletons that hold FLOW transiently) ──
address public immutable poolManager;
address public immutable positionManager;
address public immutable universalRouter;
address public hook; // FlowHook — forwards the 5% swap fee (native) to fundBaskets()
address public platformVault; // (FLOW only) redeems the 2%-positions it holds → ETH into FLOW baskets
// Launchpad tokens are deployed `restricted`: their creator gets ownership but the
// reward/forfeit-accounting setters are permanently locked (audit M-1), so a creator can't
// manipulate their token's baskets to harm buyers. FLOW itself (the flagship) is unrestricted.
bool public immutable restricted;
uint256 public initialSupply; // the amount minted at launch (for a live "% burned" readout)
// ── bonding-curve launch ──
address public curve; // LaunchCurve contract (phase-1 seller); 0 for a direct DEX launch
bool public graduated; // true once the curve has seeded the V4 pool
// Addresses that behave like a market (the pool side): transfers FROM them are
// "buys" (no forfeit on the sender), and they escape maxWallet.
mapping(address => bool) public isAMMPair;
// Pre-launch "privileged mover" allowlist (owner seeds the curve while trading is off).
mapping(address => bool) public isExcludedFromFees;
mapping(address => bool) public isExcludedFromLimits;
bool public tradingEnabled;
uint256 public maxTx;
uint256 public maxWallet;
// ───────────────────────────── events ────────────────────────────────
event BasketFilled(uint256 nativeAdded, uint256 newPerShare);
event Forfeited(address indexed who, uint256 amount, uint256 toProject, uint256 toLp, uint256 toHolders);
event BasketMoved(address indexed from, address indexed to, uint256 amount); // plain transfer carries the basket
event Redeemed(address indexed who, uint256 tokensBurned, uint256 nativePaid);
event TradingEnabled();
event HookSet(address hook);
constructor(
string memory name_, // per-launch name/symbol (the launchpad picks these)
string memory symbol_,
address treasury_,
uint256 supply_, // whole tokens, e.g. 1_000_000_000
address poolManager_,
address positionManager_,
address universalRouter_,
bool restricted_ // true for launchpad tokens (locks accounting setters)
) ERC20(name_, symbol_) Ownable(msg.sender) {
restricted = restricted_;
require(treasury_ != address(0), "zero addr");
require(poolManager_ != address(0) && positionManager_ != address(0) && universalRouter_ != address(0), "zero v4");
treasury = treasury_;
poolManager = poolManager_;
positionManager = positionManager_;
universalRouter = universalRouter_;
uint256 total = supply_ * 10 ** decimals();
initialSupply = total;
// The V4 singletons hold FLOW transiently during swaps/liquidity — treat them like
// the market side: excluded from rewards (never accrue baskets), maxWallet-exempt (via
// isAMMPair), and a transfer FROM them (a buy) doesn't forfeit the recipient.
_markInfrastructure(poolManager_);
_markInfrastructure(positionManager_);
_markInfrastructure(universalRouter_);
// never-earn / never-forfeit set
_excludeFromRewards(address(this), true);
_excludeFromRewards(treasury_, true);
_excludeFromRewards(msg.sender, true);
_excludeFromRewards(address(0xdead), true);
isExcludedFromFees[msg.sender] = true;
isExcludedFromFees[address(this)] = true;
isExcludedFromLimits[msg.sender] = true;
isExcludedFromLimits[address(this)] = true;
isExcludedFromLimits[address(0xdead)] = true;
// On a LAUNCHPAD (restricted) token the treasury IS the untrusted creator. Exempting it from
// limits would let the creator snipe their own launch past the advertised maxTx/maxWallet caps
// — and that exemption is un-removable (setters are notRestricted-locked). So only the FLAGSHIP
// (unrestricted, treasury = us) gets the treasury fee/limit exemption. (audit: creator snipe edge)
if (!restricted_) {
isExcludedFromFees[treasury_] = true;
isExcludedFromLimits[treasury_] = true;
}
// launch limits (owner can loosen after launch)
maxTx = total / 100; // 1%
maxWallet = total / 50; // 2%
_mint(msg.sender, total); // owner seeds the curve from here
}
receive() external payable {} // accept native (reward reserve) from the hook / curve
/// Mark a V4 singleton as market-side infrastructure: never accrues baskets, a transfer
/// FROM it is a "buy" (no forfeit on the recipient), and it escapes maxWallet via isAMMPair.
/// Deliberately NOT limit-exempt, so per-tx maxTx still applies to trades (mirrors the V2
/// pair, which was maxWallet-exempt but maxTx-enforced). The curve/owner big transfers are
/// exempt via their own isExcludedFromLimits, so graduation seeding isn't blocked.
function _markInfrastructure(address a) internal {
_settle(a); // crystallise + resync like every other exclusion setter, so totalShares stays
isAMMPair[a] = true; // == Σ eligible balances even if `a` already held FLOW (audit L-1)
_excludeFromRewards(a, true);
_syncShares(a);
}
// ══════════════════════════════════════════════════════════════════════
// CORE TRANSFER HOOK
// ══════════════════════════════════════════════════════════════════════
function _update(address from, address to, uint256 amount) internal override {
// Mint / burn: no forfeit — just keep baskets & shares in sync.
if (from == address(0) || to == address(0)) {
if (from != address(0)) _settle(from);
if (to != address(0)) _settle(to);
super._update(from, to, amount);
_syncShares(from);
_syncShares(to);
if (to != address(0)) _touchHoldSinceOnReceive(to, amount);
if (from != address(0)) holdSince[from] = block.timestamp; // burn = exit → reset streak
return;
}
bool isBuy = isAMMPair[from]; // pool → holder
// ── launch guards ──
// Pre-launch, only privileged (fee-exempt) addresses may move tokens — lets the
// owner seed the curve while buys/sells stay blocked.
if (!tradingEnabled) {
require(isExcludedFromFees[from] || isExcludedFromFees[to], "trading disabled");
}
// Token-level limits apply only after launch. During the curve phase the LaunchCurve
// enforces its own per-wallet cap.
if (tradingEnabled) {
if (!isExcludedFromLimits[from] && !isExcludedFromLimits[to]) {
require(amount <= maxTx, "max tx");
}
if (!isExcludedFromLimits[to] && !isAMMPair[to]) {
require(balanceOf(to) + amount <= maxWallet, "max wallet");
}
}
// ── EXIT (forfeit) vs MOVE (basket travels with the tokens) ──
// A holder's outgoing move either EXITS its basket or CARRIES it along. It's an exit — forfeit
// + redistribute — when the tokens go to the market (a sell: isAMMPair[to]) or to a
// reward-excluded address (where a basket can't live). A plain transfer to another eligible
// holder instead MOVES the proportional basket to the recipient, so moving between your own
// wallets never costs you. Selling still forfeits and can't be dodged: the basket travels to the
// new wallet and forfeits on the real sell. Runs BEFORE we settle `to` (forfeit can bump the accumulator).
if (!isBuy && !isExcludedFromRewards[from]) {
_settle(from);
if (isAMMPair[to] || isExcludedFromRewards[to]) {
_forfeit(from, amount); // sell, or a move to an excluded address
} else {
_settle(to);
_moveBasket(from, to, amount); // plain wallet-to-wallet transfer
}
}
// Settle both parties at the (possibly bumped) accumulator, on their pre-change
// balances, so the spill lands correctly.
_settle(from);
_settle(to);
// No tax here — the 5% is taken by FlowHook on swaps and pushed to fundBaskets().
super._update(from, to, amount);
_syncShares(from);
_syncShares(to);
// ── hold-time (display only) ──
holdSince[from] = block.timestamp; // any outgoing move breaks the streak
_touchHoldSinceOnReceive(to, amount); // weighted-avg for the receiver
}
// ══════════════════════════════════════════════════════════════════════
// REWARD / BASKET INTERNALS
// ══════════════════════════════════════════════════════════════════════
/// Crystallise pending accrual into basket[account]; advance its checkpoint.
function _settle(address account) internal {
if (isExcludedFromRewards[account]) {
rewardCheckpoint[account] = magnifiedRewardPerShare;
return;
}
uint256 m = magnifiedRewardPerShare;
uint256 delta = m - rewardCheckpoint[account];
if (delta > 0) {
uint256 accrued = (delta * balanceOf(account)) / MAGNITUDE;
if (accrued > 0) basket[account] += accrued;
rewardCheckpoint[account] = m;
}
}
/// Move a proportional slice of `from`'s basket to `to` on a plain transfer — the basket travels
/// with the tokens, so moving between your own wallets costs nothing. Assumes BOTH were just
/// _settle'd (so basket[] holds each side's crystallised value and no pending accrual is skipped).
function _moveBasket(address from, address to, uint256 amount) internal {
uint256 bal = balanceOf(from);
if (bal == 0) return;
uint256 b = basket[from];
if (b == 0) return;
uint256 moveAmt = (b * amount) / bal; // proportional to the fraction leaving
if (moveAmt == 0) return;
basket[from] = b - moveAmt;
basket[to] += moveAmt;
emit BasketMoved(from, to, moveAmt);
}
/// Forfeit a proportional slice of `from`'s basket when `amount` tokens leave.
/// Assumes `from` was just _settle'd. Splits 20% project / 40% buyback / 40% holders,
/// and excludes `from` from its own spill.
function _forfeit(address from, uint256 amount) internal {
uint256 bal = balanceOf(from);
if (bal == 0) return;
uint256 b = basket[from];
if (b == 0) return;
uint256 forfeitAmt = (b * amount) / bal; // proportional to fraction leaving
if (forfeitAmt == 0) return;
basket[from] = b - forfeitAmt;
uint256 toProject = (forfeitAmt * FORFEIT_PROJECT_BPS) / BPS;
uint256 toLp = (forfeitAmt * FORFEIT_LP_BPS) / BPS;
uint256 toHolders = forfeitAmt - toProject - toLp;
pendingProject += toProject;
// The buyback (processLiquidity) can only spend pendingLiquidity once the V4 pool is live (graduated).
// In production `graduated` is set at launch, so this is always the pendingLiquidity path. If a token
// ever forfeits before a pool exists (no buyback venue), route the slice to the project queue instead
// of stranding it in an unspendable one — same spirit as the "no other holders" fallback below.
if (graduated) pendingLiquidity += toLp;
else pendingProject += toLp;
// Spill the holders' slice across OTHER holders. Distribute over
// (totalShares − from's shares); advance from's checkpoint to exclude it.
uint256 eligible = totalShares > bal ? totalShares - bal : 0;
if (toHolders > 0 && eligible > 0) {
magnifiedRewardPerShare += (toHolders * MAGNITUDE) / eligible;
rewardCheckpoint[from] = magnifiedRewardPerShare; // exclude `from`
} else if (toHolders > 0) {
// no other holders → route to project rather than stranding it
pendingProject += toHolders;
}
emit Forfeited(from, forfeitAmt, toProject, toLp, toHolders);
}
/// Keep totalShares == Σ balances of reward-eligible accounts.
function _syncShares(address account) internal {
if (account == address(0)) return;
uint256 newS = isExcludedFromRewards[account] ? 0 : balanceOf(account);
uint256 old = _shares[account];
if (newS != old) {
totalShares = totalShares - old + newS;
_shares[account] = newS;
}
}
/// Weighted-average acquire time so buying more only gently lowers the streak.
function _touchHoldSinceOnReceive(address to, uint256 received) internal {
if (received == 0 || isExcludedFromRewards[to]) return;
uint256 newBal = balanceOf(to);
uint256 oldBal = newBal > received ? newBal - received : 0;
if (oldBal == 0) {
holdSince[to] = block.timestamp;
} else {
holdSince[to] = (oldBal * holdSince[to] + received * block.timestamp) / newBal;
}
}
// ══════════════════════════════════════════════════════════════════════
// REDEEM
// ══════════════════════════════════════════════════════════════════════
/// Burn `n` FLOW and withdraw the matching slice of your basket (native).
/// Exempt from the forfeit rule. Native is paid LAST (CEI) + nonReentrant.
function redeem(uint256 n) external nonReentrant {
_redeem(n);
}
/// Redeem your entire position.
function redeemAll() external nonReentrant {
_redeem(balanceOf(msg.sender));
}
function _redeem(uint256 n) internal {
_settle(msg.sender);
uint256 bal = balanceOf(msg.sender);
require(n > 0 && n <= bal, "bad amount");
uint256 owed = (basket[msg.sender] * n) / bal;
basket[msg.sender] -= owed; // effect
_burn(msg.sender, n); // effect (triggers _update burn branch)
if (owed > 0) { // interaction — LAST
(bool ok, ) = payable(msg.sender).call{value: owed}("");
require(ok, "native transfer failed");
}
emit Redeemed(msg.sender, n, owed);
}
// ══════════════════════════════════════════════════════════════════════
// NATIVE IN → BASKETS (hook / curve)
// ══════════════════════════════════════════════════════════════════════
/// The curve (phase 1), the hook (post-graduation), and the platform vault (redeemed cross-token
/// baskets) deposit native here → baskets.
function fundBaskets() external payable {
require(msg.sender == curve || msg.sender == hook || msg.sender == platformVault || msg.sender == owner(), "auth");
if (msg.value == 0) return;
if (totalShares > 0) {
magnifiedRewardPerShare += (msg.value * MAGNITUDE) / totalShares;
emit BasketFilled(msg.value, magnifiedRewardPerShare);
} else {
pendingProject += msg.value; // no holders yet → don't strand it
}
}
// ══════════════════════════════════════════════════════════════════════
// FORFEIT-FUNDS PROCESSING (keeper)
// ══════════════════════════════════════════════════════════════════════
/// Send queued project funds to the treasury.
function processProject() external nonReentrant {
uint256 amt = pendingProject;
require(amt > 0, "nothing");
pendingProject = 0;
(bool ok, ) = payable(treasury).call{value: amt}("");
require(ok, "send failed");
}
// Buyback per-call price-impact bound (on the pool's sqrtPrice). Caps how far one buyback can push
// the price, so the unspent ETH carries over — this bounds any single sandwich's extractable value
// and, combined with the call being PERMISSIONLESS, removes the owner's exclusive timing advantage.
uint256 public constant BUYBACK_MAX_SLIP_BPS = 250; // ~5% price move max per call
/// Buyback-and-burn the forfeit "liquidity" slice: swap `pendingLiquidity` ETH for FLOW on the V4
/// pool and burn it → drops supply, supports the floor. PERMISSIONLESS (anyone/any keeper may call;
/// the owner has no special power here — audit M-1). The swap is hard-bounded to
/// BUYBACK_MAX_SLIP_BPS off the pool's CURRENT price via an on-chain slot0 read; ETH not spent
/// within that bound is returned to pendingLiquidity for the next call. (The buyback itself pays the
/// hook fee, so a little flows to baskets; the rest buys & burns.)
function processLiquidity() external nonReentrant {
require(graduated, "not live"); // needs the V4 pool
uint256 amt = pendingLiquidity;
require(amt > 0, "nothing");
pendingLiquidity = 0;
IPoolManager(poolManager).unlock(abi.encode(amt));
}
/// PoolManager unlock callback for the buyback: swap ETH → FLOW (price-bounded), then burn it.
function unlockCallback(bytes calldata data) external override returns (bytes memory) {
require(msg.sender == poolManager, "auth");
uint256 amt = abi.decode(data, (uint256));
PoolKey memory key = _poolKey();
// bound the swap to BUYBACK_MAX_SLIP_BPS below the current price (ETH→FLOW lowers the price);
// getSlot0 is a live on-chain read of the pool's sqrtPrice.
// ACCEPTED RISK (audit M-2): the reference is the instantaneous slot0 price, so a same-block MEV
// manipulator can push the price up and have the buyback execute near it, leaking BOUNDED value
// (≤ BUYBACK_MAX_SLIP_BPS per call) from the buyback queue only — never holder baskets or principal,
// and unspent ETH carries over (below). A manipulation-resistant TWAP/oracle reference is tracked as
// dedicated future hardening; the per-call bound is the interim protection.
(uint160 sqrtNow, , , ) = IPoolManager(poolManager).getSlot0(key.toId());
uint160 limit = uint160((uint256(sqrtNow) * (BPS - BUYBACK_MAX_SLIP_BPS)) / BPS);
if (limit <= TickMath.MIN_SQRT_PRICE) limit = TickMath.MIN_SQRT_PRICE + 1;
BalanceDelta delta = IPoolManager(poolManager).swap(
key,
SwapParams({ zeroForOne: true, amountSpecified: -int256(amt), sqrtPriceLimitX96: limit }),
""
);
int128 d0 = delta.amount0(); // ETH we owe (negative)
int128 d1 = delta.amount1(); // FLOW we receive (positive)
uint256 ethUsed = d0 < 0 ? uint256(uint128(-d0)) : 0;
if (ethUsed > 0) key.currency0.settle(IPoolManager(poolManager), address(this), ethUsed, false);
// carry over ETH the price-bound left unspent, so it isn't stranded and backs the next buyback
if (ethUsed < amt) pendingLiquidity += (amt - ethUsed);
uint256 flowOut = d1 > 0 ? uint256(uint128(d1)) : 0;
if (flowOut > 0) {
key.currency1.take(IPoolManager(poolManager), address(this), flowOut, false); // pull FLOW here
_burn(address(this), flowOut); // real supply burn
}
return "";
}
function _poolKey() internal view returns (PoolKey memory) {
return PoolKey({
currency0: Currency.wrap(address(0)), // native ETH
currency1: Currency.wrap(address(this)),
fee: POOL_FEE,
tickSpacing: POOL_TICK_SPACING,
hooks: IHooks(hook)
});
}
// ══════════════════════════════════════════════════════════════════════
// VIEWS
// ══════════════════════════════════════════════════════════════════════
/// Tokens removed from circulation: real supply burns (redeem + buyback) plus the graduation
/// remainder parked at the dead address. For a live "X burned / Y% of supply" readout.
function burned() external view returns (uint256) {
return initialSupply - totalSupply() + balanceOf(address(0xdead));
}
/// Live basket value (crystallised + pending accrual) in native wei.
function basketOf(address account) public view returns (uint256) {
if (isExcludedFromRewards[account]) return basket[account];
uint256 delta = magnifiedRewardPerShare - rewardCheckpoint[account];
uint256 pending = (delta * balanceOf(account)) / MAGNITUDE;
return basket[account] + pending;
}
/// Seconds the wallet has held its current position (0 after any move-out).
function holdDuration(address account) external view returns (uint256) {
uint256 hs = holdSince[account];
return hs == 0 ? 0 : block.timestamp - hs;
}
// ══════════════════════════════════════════════════════════════════════
// ADMIN
// ══════════════════════════════════════════════════════════════════════
function enableTrading() external onlyOwner {
require(!tradingEnabled, "already");
// If a curve is set, the DEX may only open via graduation — prevents opening a
// second market before the curve seeds the V4 pool at the final price.
require(curve == address(0) || graduated, "use curve");
tradingEnabled = true;
emit TradingEnabled();
}
/// Wire the FlowHook (authorised to fundBaskets with the swap fee). Excluded like infra.
function setHook(address h) external onlyOwner {
require(hook == address(0), "hook set"); // one-shot: can't swap the hook post-launch (audit M-2)
require(h != address(0), "zero");
hook = h;
_markInfrastructure(h);
emit HookSet(h);
}
/// One-shot: authorise the PlatformVault to stream redeemed cross-token baskets into FLOW baskets.
/// (Only meaningful on the FLOW platform token.)
function setPlatformVault(address v) external onlyOwner notRestricted {
require(platformVault == address(0) && v != address(0), "set");
platformVault = v;
}
// ── bonding-curve wiring ──
/// Register the LaunchCurve. It sells FLOW in phase 1, so it acts like a market
/// (sells forfeit) and is fee/limit exempt (its fee is charged in native).
function setCurve(address c) external onlyOwner {
require(curve == address(0) && c != address(0), "curve set");
require(!tradingEnabled, "already live");
require(ICurve(c).token() == address(this), "bad curve"); // reject EOA/typo → no brick
curve = c;
isAMMPair[c] = true;
isExcludedFromFees[c] = true;
isExcludedFromLimits[c] = true;
_settle(c);
_excludeFromRewards(c, true);
_syncShares(c);
}
/// Called by the curve at graduation: opens trading.
function onGraduate() external {
require(msg.sender == curve, "auth");
graduated = true;
if (!tradingEnabled) {
tradingEnabled = true;
emit TradingEnabled();
}
}
// Restricted (launchpad) tokens can only LOOSEN limits (removeLimits), never re-tighten them —
// so a creator can't grief holders by clamping trading after launch. (audit M-1)
function setLimits(uint256 maxTx_, uint256 maxWallet_) external onlyOwner notRestricted {
require(maxTx_ >= totalSupply() / 1000 && maxWallet_ >= totalSupply() / 1000, "too low");
maxTx = maxTx_;
maxWallet = maxWallet_;
}
function removeLimits() external onlyOwner {
maxTx = type(uint256).max;
maxWallet = type(uint256).max;
}
/// Reward/forfeit-accounting setters below are permanently locked on `restricted` (launchpad)
/// tokens, so a creator who receives ownership cannot manipulate baskets to harm buyers (audit M-1).
/// The V4 singletons (PoolManager/PositionManager/UniversalRouter) are already market-side from the
/// constructor, so a restricted token needs none of these post-launch.
modifier notRestricted() {
require(!restricted, "restricted");
_;
}
function setAMMPair(address p, bool v) external onlyOwner notRestricted {
require(p != poolManager, "core pool");
if (!v) require(!_isProtected(p), "protected"); // don't re-include protected addrs
isAMMPair[p] = v;
_settle(p);
_excludeFromRewards(p, v);
_syncShares(p);
}
/// Addresses that must never become reward-eligible (would strand value or break the
/// totalShares invariant if re-included via a setter).
function _isProtected(address a) internal view returns (bool) {
return a == address(this)
|| a == poolManager
|| a == positionManager
|| a == universalRouter
|| a == hook
|| a == curve
|| a == address(0xdead);
}
function setExcludedFromFees(address a, bool v) external onlyOwner notRestricted {
isExcludedFromFees[a] = v;
}
function setExcludedFromLimits(address a, bool v) external onlyOwner notRestricted {
isExcludedFromLimits[a] = v;
}
function setExcludedFromRewards(address a, bool v) external onlyOwner notRestricted {
if (!v) require(!_isProtected(a), "protected"); // can't make protected addrs eligible
_settle(a);
_excludeFromRewards(a, v);
_syncShares(a);
}
function setTreasury(address t) external onlyOwner {
require(t != address(0), "zero");
treasury = t;
}
function _excludeFromRewards(address a, bool v) internal {
isExcludedFromRewards[a] = v;
rewardCheckpoint[a] = magnifiedRewardPerShare;
}
}
Chain explorer2764msChain node86ms