KaleidoToken
0x6689ab375aaaa9aed78aaaba2e8edcc547478cf1
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
14,849 bytes
ABI entries
109
43 read · 19 write
License
mit
Contract information
- Address
- 0x6689ab375aaaa9aed78aaaba2e8edcc547478cf1
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xa2445bd356…7f032a3449
- Creation tx
- 0x6bba72749b…93633d805e
Token
- Name
- Kaleido
- Symbol
- KALEIDO
- Decimals
- 18
- Holders
- 634
- Market
- View token page →
Read contract (43)
DOMAIN_SEPARATOR() → bytes32
POOL_FEE() → uint24
SUPPLY() → uint256
TICK_SPACING() → int24
UNIT() → uint256
accEthPerShare() → uint256
allowance(address, address) → uint256
balanceOf(address) → uint256
bountyFor(uint256) → uint256
claimRouter() → address
decimals() → uint8
dev() → address
devOwed() → uint256
feeDebt(uint256) → uint256
forfeitedWei() → uint256
globalTickLower() → int24
globalTickUpper() → int24
kaleidoIsToken0() → bool
mintedAt(uint256) → uint256
mirror() → address
name() → string
nftBalanceOf(address) → uint256
nftContractURI() → string
nftGetApproved(uint256) → address
nftIsApprovedForAll(address, address) → bool
nftOwnerOf(uint256) → address
nftTokenURI(uint256) → string
nonces(address) → uint256
npm() → address
onERC721Received(address, address, uint256, bytes) → bytes4
ownedTokensOf(address) → uint256[]
owner() → address
ownershipHandoverExpiresAt(address) → uint256
pendingFees(uint256) → uint256
pool() → address
positionTokenId() → uint256
renderer() → address
seeded() → bool
symbol() → string
totalShares() → uint256
totalSupply() → uint256
v3Factory() → address
weth() → address
Events (15)
ApprovalClaimRouterSetDevWithdrawnFeesHarvestedFeesSettledForfeitedForfeitsSweptNFTBurnedNFTMintedOwnershipHandoverCanceledOwnershipHandoverRequestedOwnershipTransferredRendererSetSeededTransfer
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_v3Factory",
"type": "address"
},
{
"internalType": "address",
"name": "_npm",
"type": "address"
},
{
"internalType": "address",
"name": "_weth",
"type": "address"
},
{
"internalType": "address",
"name": "_owner",
"type": "address"
},
{
"internalType": "address",
"name": "_dev",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AllowanceOverflow",
"type": "error"
},
{
"inputs": [],
"name": "AllowanceUnderflow",
"type": "error"
},
{
"inputs": [],
"name": "AlreadyInitialized",
"type": "error"
},
{
"inputs": [],
"name": "AlreadySeeded",
"type": "error"
},
{
"inputs": [],
"name": "ClaimRouterAlreadySet",
"type": "error"
},
{
"inputs": [],
"name": "ClaimRouterOnly",
"type": "error"
},
{
"inputs": [],
"name": "EthTransferFailed",
"type": "error"
},
{
"inputs": [],
"name": "ExcludedShareRecipient",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientAllowance",
"type": "error"
},
{
"inputs": [],
"name": "InsufficientBalance",
"type": "error"
},
{
"inputs": [],
"name": "InvalidPermit",
"type": "error"
},
{
"inputs": [],
"name": "InvalidTokenId",
"type": "error"
},
{
"inputs": [],
"name": "MirrorOnly",
"type": "error"
},
{
"inputs": [],
"name": "NewOwnerIsZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "NoHandoverRequest",
"type": "error"
},
{
"inputs": [],
"name": "NotDev",
"type": "error"
},
{
"inputs": [],
"name": "NotOwnerOrApproved",
"type": "error"
},
{
"inputs": [],
"name": "NotShareOwner",
"type": "error"
},
{
"inputs": [],
"name": "Permit2AllowanceIsFixedAtInfinity",
"type": "error"
},
{
"inputs": [],
"name": "PermitExpired",
"type": "error"
},
{
"inputs": [],
"name": "Reentrancy",
"type": "error"
},
{
"inputs": [],
"name": "RouterHandshakeFailed",
"type": "error"
},
{
"inputs": [],
"name": "SeedNotSingleSided",
"type": "error"
},
{
"inputs": [],
"name": "SeedPoolMismatch",
"type": "error"
},
{
"inputs": [],
"name": "SeedPriceMismatch",
"type": "error"
},
{
"inputs": [],
"name": "SelfTransferDisallowed",
"type": "error"
},
{
"inputs": [],
"name": "TotalSupplyOverflow",
"type": "error"
},
{
"inputs": [],
"name": "TransferToZero",
"type": "error"
},
{
"inputs": [],
"name": "Unauthorized",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"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": "amount",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "router",
"type": "address"
}
],
"name": "ClaimRouterSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DevWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "ethToHolders",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethToDev",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "bounty",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenBurned",
"type": "uint256"
}
],
"name": "FeesHarvested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "holder",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokenCount",
"type": "uint256"
}
],
"name": "FeesSettled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Forfeited",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "ethToHolders",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "bounty",
"type": "uint256"
}
],
"name": "ForfeitsSwept",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "NFTBurned",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "NFTMinted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "pendingOwner",
"type": "address"
}
],
"name": "OwnershipHandoverCanceled",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "pendingOwner",
"type": "address"
}
],
"name": "OwnershipHandoverRequested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "oldOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "renderer",
"type": "address"
}
],
"name": "RendererSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "posmTokenId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"indexed": false,
"internalType": "uint128",
"name": "liquidity",
"type": "uint128"
}
],
"name": "Seeded",
"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": "Transfer",
"type": "event"
},
{
"inputs": [],
"name": "DOMAIN_SEPARATOR",
"outputs": [
{
"internalType": "bytes32",
"name": "result",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POOL_FEE",
"outputs": [
{
"internalType": "uint24",
"name": "",
"type": "uint24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SUPPLY",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "TICK_SPACING",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "UNIT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "accEthPerShare",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "result",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "result",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "ethGained",
"type": "uint256"
}
],
"name": "bountyFor",
"outputs": [
{
"internalType": "uint256",
"name": "b",
"type": "uint256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "cancelOwnershipHandover",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "claimRouter",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pendingOwner",
"type": "address"
}
],
"name": "completeOwnershipHandover",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "dev",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "devOwed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "feeDebt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "forfeitedWei",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "globalTickLower",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "globalTickUpper",
"outputs": [
{
"internalType": "int24",
"name": "",
"type": "int24"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "caller",
"type": "address"
}
],
"name": "handleNFTApprove",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
},
{
"internalType": "address",
"name": "caller",
"type": "address"
}
],
"name": "handleNFTSetApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "address",
"name": "caller",
"type": "address"
}
],
"name": "handleNFTTransfer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "kaleidoIsToken0",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "mintedAt",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "mirror",
"outputs": [
{
"internalType": "contract KaleidoMirror",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner_",
"type": "address"
}
],
"name": "nftBalanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "nftContractURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "nftGetApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "o",
"type": "address"
},
{
"internalType": "address",
"name": "s",
"type": "address"
}
],
"name": "nftIsApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "nftOwnerOf",
"outputs": [
{
"internalType": "address",
"name": "o",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "nftTokenURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "nonces",
"outputs": [
{
"internalType": "uint256",
"name": "result",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "npm",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"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": [
{
"internalType": "address",
"name": "owner_",
"type": "address"
}
],
"name": "ownedTokensOf",
"outputs": [
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "result",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pendingOwner",
"type": "address"
}
],
"name": "ownershipHandoverExpiresAt",
"outputs": [
{
"internalType": "uint256",
"name": "result",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "pendingFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "uint8",
"name": "v",
"type": "uint8"
},
{
"internalType": "bytes32",
"name": "r",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "s",
"type": "bytes32"
}
],
"name": "permit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "pokeFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "pool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "positionTokenId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renderer",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "requestOwnershipHandover",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
}
],
"name": "seed",
"outputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "seeded",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "router",
"type": "address"
}
],
"name": "setClaimRouter",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newRenderer",
"type": "address"
}
],
"name": "setRenderer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
},
{
"internalType": "address",
"name": "holder",
"type": "address"
}
],
"name": "settleClaim",
"outputs": [
{
"internalType": "uint256",
"name": "ethOut",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "sweepForfeits",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "totalShares",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "result",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"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": "amount",
"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": "payable",
"type": "function"
},
{
"inputs": [],
"name": "v3Factory",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "weth",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "withdrawDev",
"outputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.26;
// Derived from Prism's PrismHook (https://github.com/0xsolazy/prism, MIT © 0xsolazy). See docs/fork-map.md.
// KALEIDO — the protocol token ($KALEIDO) and its NFT "Shares" (final brand, VEG2-1148; historical code name
// "Quotra/QOTA" survives only in git history / changelog).
//
// The Kaleido core (monolith, per fork-map §5): ERC-20 (5000 KALEIDO) + dual ERC-20/721 NFT accounting
// (the Shares) + owner of the protocol LP position. Forked from PrismHook with the v4 hook REMOVED —
// the KALEIDO/ETH pool is vanilla (hooks = address(0)); closes the SATO drift class.
//
// v1.1 "Choose your dividend" (amendment 10/07): no vote, no epochs, no calendar. The Prism-vanilla per-share
// ETH accumulator (accEthPerShare + feeDebt) returns in VEG2-1121/1125 (pokeFees credits it directly); holders
// pick their stock at claim time (ClaimRouter). `mintedAt` is kept as a stats field only — no eligibility, the
// accumulator/feeDebt couple makes JIT impossible by construction.
import { ERC20 } from "solady/src/tokens/ERC20.sol";
import { Ownable } from "solady/src/auth/Ownable.sol";
import { ReentrancyGuard } from "solady/src/utils/ReentrancyGuard.sol";
import { KaleidoMirror } from "./KaleidoMirror.sol";
// The protocol pool is Uniswap v3 (VEG2-1150): DexScreener indexes v2/v3 on Robinhood Chain but not v4, and the
// chart is the acquisition funnel — so KALEIDO's OWN pool launches on v3 (native chart day 1). The claim swaps
// (ClaimRouter/V4SwapLib) stay on v4 — the protocol is bi-venue. Minimal v3 surfaces, vendored inline (no new
// submodule): the NonfungiblePositionManager (create/mint the position, collect fees) and WETH9 (unwrap fees).
interface INonfungiblePositionManager {
function createAndInitializePoolIfNecessary(address token0, address token1, uint24 fee, uint160 sqrtPriceX96)
external
payable
returns (address pool);
struct MintParams {
address token0;
address token1;
uint24 fee;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
function mint(MintParams calldata params)
external
payable
returns (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
struct CollectParams {
uint256 tokenId;
address recipient;
uint128 amount0Max;
uint128 amount1Max;
}
function collect(CollectParams calldata params) external payable returns (uint256 amount0, uint256 amount1);
}
interface IWETH9 {
function withdraw(uint256) external;
}
/// @dev v3 factory + pool slot0 — used ONLY in `seed` to prove the pool the NPM returned is the canonical one and
/// that it was initialized at exactly the launch price (H-01: the token address is predictable, so an attacker
/// can front-run the pool init). Never touched on a hot path.
interface IUniswapV3Factory {
function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address);
}
interface IUniswapV3PoolSlot0 {
function slot0() external view returns (uint160 sqrtPriceX96, int24 tick, uint16, uint16, uint16, uint8, bool);
}
/// @dev External, replaceable metadata renderer for the Shares (VEG2-1129/§4). Only the POINTER is part of the
/// frozen ABI; the art contract is a separate, post-freeze ticket and can be re-pointed. Called from views
/// only (see nftTokenURI / nftContractURI), never from a money or transfer path.
interface IKaleidoRenderer {
function tokenURI(uint256 tokenId) external view returns (string memory);
function contractURI() external view returns (string memory);
}
contract KaleidoToken is ERC20, Ownable, ReentrancyGuard {
/* ERRORS */
error AlreadySeeded();
error ZeroAddress();
error NotOwnerOrApproved();
error InvalidTokenId();
error MirrorOnly();
error TransferToZero();
error ExcludedShareRecipient();
error SelfTransferDisallowed();
error EthTransferFailed();
error ClaimRouterOnly();
error ClaimRouterAlreadySet();
error NotShareOwner();
error SeedPoolMismatch();
error SeedPriceMismatch();
error SeedNotSingleSided();
error RouterHandshakeFailed();
error NotDev();
/* EVENTS */
event Seeded(uint256 indexed posmTokenId, uint160 sqrtPriceX96, uint128 liquidity);
event NFTMinted(address indexed to, uint256 indexed tokenId);
event NFTBurned(address indexed from, uint256 indexed tokenId);
event FeesHarvested(uint256 ethToHolders, uint256 ethToDev, uint256 bounty, uint256 tokenBurned);
event ClaimRouterSet(address indexed router);
event FeesSettled(address indexed holder, uint256 ethOut, uint256 tokenCount);
/// @dev `amount` reports the GROSS forfeited pending of the burned Share (its pre-floor accrued dividend), not
/// the net the survivors ultimately claim — the sub-wei rounding remainder stays custodied as invariant dust
/// and is never re-emitted (the net is anyway unknowable at burn: it depends on the survivors' own claim floors).
event Forfeited(uint256 indexed tokenId, uint256 amount);
event ForfeitsSwept(uint256 ethToHolders, uint256 bounty);
event RendererSet(address indexed renderer);
event DevWithdrawn(address indexed to, uint256 amount);
/* CONSTANTS */
/// @dev Token metadata constants — centralized (final brand, VEG2-1148).
string internal constant NAME = "Kaleido";
string internal constant SYMBOL = "KALEIDO";
uint256 public constant SUPPLY = 5000 ether;
uint256 public constant UNIT = 1 ether;
uint24 public constant POOL_FEE = 10_000; // 1% static
int24 public constant TICK_SPACING = 200;
/// @dev Per-share ETH fee accumulator scale (Prism-vanilla). 1e12 keeps per-share amounts in range.
uint256 internal constant ACC_SCALE = 1e12;
/// @dev pokeFees caller bounty = 0.25% of the ETH harvested, capped at BOUNTY_CAP (documented absolute cap —
/// poking is cheap-to-incentivize, not a yield source; the cap stops a big accrued harvest over-paying one poker).
uint256 internal constant BOUNTY_BPS = 25;
uint256 internal constant BOUNTY_CAP = 0.05 ether;
/// @dev Of the post-bounty ETH: 90% to holders (accEthPerShare); the remaining 10% is accrued to `devOwed`.
uint256 internal constant HOLDER_BPS = 9000;
/// @dev H-01: max KALEIDO left in the reserve after the single-sided mint before `seed` reverts. A real fresh
/// mainnet-4663 mint strands just 4 wei of rounding (measured, `test_Fork_V3_SeedSingleSided`); this 1 gwei
/// tolerance is ~2.5e8x that headroom (safe across launch-tick variance) yet still reverts any material partial
/// fill — the reserve can never be recovered (seed is one-shot). See docs/security-notes.md §7.
uint256 internal constant SEED_MAX_DUST = 1e9;
/// @dev Transient slot for the realignment-skip flag.
uint256 private constant _SILENT_SLOT = 0x9a8f8d1e3c5b7a9d6e2f4a8c7b5d3f9e1a6c4b8d2e7f5a9c3b1d6e8f2a4c7b9d;
/* IMMUTABLES */
/// @dev Uniswap v3 NonfungiblePositionManager — creates/holds the KALEIDO/WETH position and collects its fees.
address public immutable npm;
/// @dev Canonical wrapped native — the pool's counter-asset (v3 has no native ETH); fees in it are unwrapped.
address public immutable weth;
/// @dev Uniswap v3 factory (reference; the pool is created via the NPM). Exposed for the frontend/explorer.
address public immutable v3Factory;
/// @dev Fixed token ordering vs WETH (v3 pools sort by address). Decides which side of a collect is WETH.
bool public immutable kaleidoIsToken0;
KaleidoMirror public immutable mirror;
/// @dev Recipient of the 10% dev share of harvested ETH fees. Immutable — no admin path can redirect fees.
address public immutable dev;
/* PACKED STORAGE */
bool public seeded;
uint256 public positionTokenId;
int24 public globalTickLower;
int24 public globalTickUpper;
/// @dev The v3 KALEIDO/WETH pool — set in `seed`. Excluded from Share accounting in `_afterTokenTransfer`
/// (it is the reserve holder, like the v4 PoolManager was). address(0) until seeded.
address public pool;
/// @dev Monotonic ERC-721 id counter. First minted id is 1.
uint256 private _nextTokenId;
/// @dev Live NFT count = share denominator.
uint256 public totalShares;
/// @dev Packed owner + ownedIndex: [0..159]=owner, [160..191]=ownedIndex.
mapping(uint256 => uint256) private _oo;
/// @dev Per-user packed owned tokenIds, 8 uint32 per slot.
mapping(address => mapping(uint256 => uint256)) private _ownedSlots;
/// @dev Packed per-user metadata: [0..31]=ownedLength.
mapping(address => uint256) private _addressData;
mapping(uint256 => address) private _nftApprovals;
mapping(address => mapping(address => bool)) private _nftOperatorApprovals;
/// @dev Acquisition timestamp of each live tokenId — stats/provenance only (v1.1 dropped vote/claim
/// eligibility; the accumulator/feeDebt couple makes JIT impossible by construction). Set on mint + move.
mapping(uint256 => uint256) public mintedAt;
/// @dev Cumulative ETH-per-live-Share, scaled by ACC_SCALE (Prism-vanilla). Monotonic; credited in pokeFees.
uint256 public accEthPerShare;
/// @dev Per-tokenId reward debt (snapshot of accEthPerShare at mint). Claimable = accEthPerShare - feeDebt
/// (read by ClaimRouter, VEG2-1125). Setting it at mint stops a fresh Share from claiming past accrual (JIT).
mapping(uint256 => uint256) public feeDebt;
/// @dev The ClaimRouter (VEG2-1125) — the only address allowed to settle pending fees and pull the backing
/// ETH. Wired one-shot post-deploy (core and router reference each other). address(0) until set.
address public claimRouter;
/// @dev Parked forfeit pot (VEG2-1126): unclaimed pending ETH of Shares that burned with NO survivor to
/// inherit it (the last Share out). Normal burns credit survivors inline in `_burnNFT` (no window to game);
/// only this degenerate case parks here, awaiting `sweepForfeits` once a Share returns — which dodges a
/// divide-by-zero. The v1.1 "forfeited" term of the master invariant.
uint256 public forfeitedWei;
/// @dev H-02: the 10% dev fee share is ACCRUED here (not pushed) so a dev address that rejects ETH can never
/// block a harvest. `dev` pulls it via `withdrawDev`. A backed liability — part of the master conservation
/// (core ETH == holder-owed + devOwed + dust); cleared on withdraw.
uint256 public devOwed;
/// @dev Replaceable metadata renderer (VEG2-1129). address(0) => the Shares report an empty tokenURI (the
/// pre-renderer default). Owner-settable and RE-settable (SPEC §4 "upgradeable"); read from views only.
address public renderer;
/* PACKING HELPERS */
function _ownerOf(uint256 id) internal view returns (address) {
return address(uint160(_oo[id]));
}
function _ownedIndexOf(uint256 id) internal view returns (uint32) {
return uint32(_oo[id] >> 160);
}
function _setOo(uint256 id, address owner_, uint32 index) internal {
_oo[id] = uint256(uint160(owner_)) | (uint256(index) << 160);
}
function _ownedGet(address user, uint256 index) internal view returns (uint32) {
return uint32(_ownedSlots[user][index >> 3] >> ((index & 7) << 5));
}
function _ownedSet(address user, uint256 index, uint32 value) internal {
uint256 slotIdx = index >> 3;
uint256 shift = (index & 7) << 5;
uint256 word = _ownedSlots[user][slotIdx];
_ownedSlots[user][slotIdx] = (word & ~(uint256(0xFFFFFFFF) << shift)) | (uint256(value) << shift);
}
function _ownedLength(address user) internal view returns (uint32) {
return uint32(_addressData[user]);
}
function _setOwnedLength(address user, uint32 len) internal {
uint256 v = _addressData[user];
_addressData[user] = (v & ~uint256(0xFFFFFFFF)) | uint256(len);
}
/* CONSTRUCTOR */
constructor(address _v3Factory, address _npm, address _weth, address _owner, address _dev) {
if (
_v3Factory == address(0) || _npm == address(0) || _weth == address(0) || _owner == address(0)
|| _dev == address(0)
) {
revert ZeroAddress();
}
v3Factory = _v3Factory;
npm = _npm;
weth = _weth;
dev = _dev;
// v3 pools sort tokens by address; fix which side of the pair (and of a fee collect) is KALEIDO.
kaleidoIsToken0 = address(this) < _weth;
mirror = new KaleidoMirror(address(this));
_initializeOwner(_owner);
// §12 = 100% pool: the full supply is minted to the contract ("the pool is the reserve").
_mint(address(this), SUPPLY);
}
/* ERC20 METADATA */
function name() public pure override returns (string memory) {
return NAME;
}
function symbol() public pure override returns (string memory) {
return SYMBOL;
}
/// @notice L-02: KALEIDO does NOT use Permit2 (its pool is v3 via the NPM; claims settle in native ETH). Opt out
/// of Solady's default infinite Permit2 allowance so `allowance(holder, Permit2)` is a real 0 and no
/// Permit2 compromise can move a holder's balance without an explicit approval.
function _givePermit2InfiniteAllowance() internal pure override returns (bool) {
return false;
}
/* SEED */
/// @notice One-time: create+initialize the KALEIDO/WETH v3 pool and mint the single-sided protocol position
/// (100% KALEIDO, "the pool is the reserve"). The caller supplies the geometry so the current price
/// sits at the KALEIDO-heavy boundary of `[tickLower, tickUpper]` — so all SUPPLY enters as KALEIDO and
/// no WETH is needed. The position NFT is minted to and permanently held by this contract; there is NO
/// path that decreases/burns/transfers it, so protocol liquidity can never be withdrawn.
function seed(uint160 sqrtPriceX96, int24 tickLower, int24 tickUpper) external onlyOwner returns (uint256 tokenId) {
if (seeded) revert AlreadySeeded();
seeded = true;
globalTickLower = tickLower;
globalTickUpper = tickUpper;
(address token0, address token1) = kaleidoIsToken0 ? (address(this), weth) : (weth, address(this));
// let the NPM pull the single-sided KALEIDO from the reserve (self-allowance, one-shot amount).
// ⚠️ INVARIANT (load-bearing): the contract NEVER approves WETH to the NPM. So the mint can only spend
// KALEIDO — if the geometry ever implied a WETH leg, the mint reverts (retryable, seeded rolls back)
// instead of pulling WETH. This is what enforces single-sidedness by construction. Do not add a WETH
// approval here.
_approve(address(this), npm, SUPPLY);
pool =
INonfungiblePositionManager(npm).createAndInitializePoolIfNecessary(token0, token1, POOL_FEE, sqrtPriceX96);
// H-01: the token (hence pool) address is predictable, so an attacker can pre-create + initialize the
// canonical pool before this runs. createAndInitializePoolIfNecessary leaves an ALREADY-initialized pool at
// its EXISTING price and returns it unchanged. So prove (a) it is the factory's canonical pool for our key,
// and (b) it sits at EXACTLY the launch price we supplied. A mismatch means the init was front-run — revert
// rather than launch the market at the attacker's price (seeded rolls back; redeploy at a fresh address).
if (pool != IUniswapV3Factory(v3Factory).getPool(token0, token1, POOL_FEE)) revert SeedPoolMismatch();
(uint160 poolPrice,,,,,,) = IUniswapV3PoolSlot0(pool).slot0();
if (poolPrice != sqrtPriceX96) revert SeedPriceMismatch();
uint128 liquidity;
(liquidity, tokenId) = _seedMint(token0, token1, tickLower, tickUpper);
positionTokenId = tokenId;
// revoke the residual self-allowance so nothing can ever pull the leftover reserve KALEIDO to the NPM.
_approve(address(this), npm, 0);
emit Seeded(tokenId, sqrtPriceX96, liquidity);
}
/// @dev The single-sided mint + its H-01 result checks, split out of `seed` to keep the stack shallow. Reverts
/// unless the mint locked nonzero liquidity, pulled NO WETH, and consumed all but <= SEED_MAX_DUST of the
/// reserve KALEIDO (no gross partial fill — seed is one-shot, so any stranded reserve is permanent).
function _seedMint(address token0, address token1, int24 tickLower, int24 tickUpper)
private
returns (uint128 liquidity, uint256 tokenId)
{
// single-sided: SUPPLY on the KALEIDO side, 0 on the WETH side. Price is pinned in `seed` → no slippage.
(uint256 amount0Desired, uint256 amount1Desired) = kaleidoIsToken0 ? (SUPPLY, uint256(0)) : (uint256(0), SUPPLY);
INonfungiblePositionManager.MintParams memory mp = INonfungiblePositionManager.MintParams({
token0: token0,
token1: token1,
fee: POOL_FEE,
tickLower: tickLower,
tickUpper: tickUpper,
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp + 60
});
uint256 amount0;
uint256 amount1;
(tokenId, liquidity, amount0, amount1) = INonfungiblePositionManager(npm).mint(mp);
// H-01: prove the mint locked a real, single-sided, (almost) whole reserve — nonzero liquidity, NO WETH leg
// pulled, and no gross partial fill leaving reserve permanently stranded. SEED_MAX_DUST is the rounding
// tolerance; a real fresh mint strands only a few wei (fork-calibrated, see docs/security-notes.md).
uint256 wethUsed = kaleidoIsToken0 ? amount1 : amount0;
uint256 kaleidoUsed = kaleidoIsToken0 ? amount0 : amount1;
if (liquidity == 0 || wethUsed != 0 || SUPPLY - kaleidoUsed > SEED_MAX_DUST) revert SeedNotSingleSided();
}
/* FEE HARVEST */
/// @notice Permissionless fee harvest (money path #1). Collects the v3 position's accrued LP fees (both
/// tokens; `collect` touches fees only, never liquidity), unwraps the WETH side to ETH, then — SATO
/// rule (invariant #2), on MEASURED deltas only — pays the caller a capped 0.25% bounty, credits 90%
/// of the rest to `accEthPerShare`, ACCRUES 10% to `devOwed` (H-02: pull, not push — `dev` withdraws via
/// `withdrawDev`), and burns 100% of the KALEIDO-side fees. Clean no-op when nothing accrued (never
/// reverts, never a phantom bounty). CEI: all accounting effects precede the single ETH send (the
/// bounty); `nonReentrant`.
function pokeFees() external nonReentrant {
if (!seeded || totalShares == 0) return;
uint256 ethBefore = address(this).balance;
uint256 kaleidoBefore = balanceOf(address(this));
// v3 fee collection: pull ALL owed fees of the position here (both tokens). Fees only — no liquidity moves.
INonfungiblePositionManager.CollectParams memory cp = INonfungiblePositionManager.CollectParams({
tokenId: positionTokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
});
(uint256 amount0, uint256 amount1) = INonfungiblePositionManager(npm).collect(cp);
// Unwrap ONLY the WETH this collect returned (SATO: never unwrap/credit WETH that was merely donated to
// the contract). withdraw() sends ETH to receive() (empty) — nonReentrant + measuring AFTER the unwrap
// means the ETH is on-balance and can't be double-counted.
uint256 wethCollected = kaleidoIsToken0 ? amount1 : amount0;
if (wethCollected > 0) IWETH9(weth).withdraw(wethCollected);
// SATO rule: credit ONLY measured deltas. Donations (raw ETH already held, WETH beyond `wethCollected`,
// or KALEIDO) sit outside these deltas and are never credited.
uint256 ethGained = address(this).balance - ethBefore;
uint256 tokenBurned = balanceOf(address(this)) - kaleidoBefore;
uint256 bounty;
uint256 toHolders;
uint256 toDev;
// effects
if (ethGained > 0) {
bounty = bountyFor(ethGained);
uint256 net = ethGained - bounty;
toHolders = net * HOLDER_BPS / 10_000;
toDev = net - toHolders;
accEthPerShare += toHolders * ACC_SCALE / totalShares;
// H-02: ACCRUE the dev share (pull, not push) so a dev address that rejects ETH can never brick a
// harvest. dev withdraws via withdrawDev; the wei stays in the core backing devOwed.
devOwed += toDev;
}
if (tokenBurned > 0) _burn(address(this), tokenBurned);
// interaction (after all effects): ONLY the bounty is pushed — to msg.sender, who by calling accepts it, so
// a poker that rejects ETH fails only its own tx, never the system. The dev share is pull-based (above).
if (bounty > 0) _sendEth(msg.sender, bounty);
if (ethGained > 0 || tokenBurned > 0) emit FeesHarvested(toHolders, toDev, bounty, tokenBurned);
}
/// @notice The poke bounty for a given ETH harvest: 0.25%, capped at BOUNTY_CAP. Pure — also safe for UIs.
function bountyFor(uint256 ethGained) public pure returns (uint256 b) {
b = ethGained * BOUNTY_BPS / 10_000;
if (b > BOUNTY_CAP) b = BOUNTY_CAP;
}
/// @notice Permissionless (money path #2, VEG2-1126): drain the PARKED forfeit pot into `accEthPerShare` for
/// the current survivors, minus a capped 0.25% keeper bounty (same policy/dust as pokeFees). The pot is
/// only ever non-zero from the degenerate case where the LAST Share burned with pending owed (normal
/// burns credit survivors inline in `_burnNFT`, leaving no JIT window). It therefore parks until a
/// Share exists again; the first returning holder(s) receive this orphaned ETH — acceptable, as by
/// definition there were no survivors to owe it to. Clean no-op when the pot is empty OR no Shares are
/// live. CEI: pot cleared + accumulator credited before the bounty send; `nonReentrant`.
function sweepForfeits() external nonReentrant {
uint256 amount = forfeitedWei;
if (amount == 0 || totalShares == 0) return;
forfeitedWei = 0; // effect
uint256 bounty = bountyFor(amount);
uint256 toHolders = amount - bounty;
accEthPerShare += toHolders * ACC_SCALE / totalShares; // effect
if (bounty > 0) _sendEth(msg.sender, bounty); // interaction
emit ForfeitsSwept(toHolders, bounty);
}
/// @notice H-02: `dev` pulls its accrued 10% fee share to a recipient of its choice. Pull, not push, so a
/// rejecting dev address can never brick the harvest path; `dev`-only, CEI, nonReentrant.
function withdrawDev(address to) external nonReentrant returns (uint256 amount) {
if (msg.sender != dev) revert NotDev();
if (to == address(0)) revert ZeroAddress();
amount = devOwed;
devOwed = 0; // effect before interaction (CEI)
if (amount > 0) {
emit DevWithdrawn(to, amount);
_sendEth(to, amount); // interaction
}
}
function _sendEth(address to, uint256 amount) private {
(bool ok,) = to.call{ value: amount }("");
if (!ok) revert EthTransferFailed();
}
/* CLAIM SETTLEMENT (VEG2-1125) */
/// @notice One-shot wiring of the ClaimRouter. The core and the router reference each other, so the router
/// is deployed second and set here. Owner-only and immutable-after-set — once live, no admin path can
/// redirect where settled fees flow.
function setClaimRouter(address router) external onlyOwner {
if (router == address(0)) revert ZeroAddress();
if (claimRouter != address(0)) revert ClaimRouterAlreadySet();
// M-03: the one-shot is irreversible and a bad value permanently disables every claim, so prove the router
// is a real contract that points BACK to this core — not an EOA, a wrong-chain copy, or a mismatched
// instance. Low-level staticcall (not a typed call) so a fallback returning malformed/short/empty data is
// rejected CLEANLY with RouterHandshakeFailed rather than bubbling an opaque ABI-decode revert; require the
// return to be exactly 32 bytes decoding to this core. A missing/reverting getter fails the same way.
if (router.code.length == 0) revert RouterHandshakeFailed();
(bool ok, bytes memory ret) = router.staticcall(abi.encodeWithSignature("core()"));
if (!ok || ret.length != 32 || abi.decode(ret, (address)) != address(this)) revert RouterHandshakeFailed();
claimRouter = router;
emit ClaimRouterSet(router);
}
/// @notice Set (or clear, with address(0)) the Shares' metadata renderer. Owner-only and intentionally
/// RE-settable — SPEC §4 keeps the art "upgradeable"; the renderer is external and its output is
/// degraded-safe (see nftTokenURI). This is the owner's ONLY residual power once `seed` +
/// `setClaimRouter` are done, and it is purely cosmetic — it can never touch accrual, claims, the
/// registry, or the backing ETH. Renouncing ownership afterwards permanently freezes the art pointer.
function setRenderer(address newRenderer) external onlyOwner {
renderer = newRenderer;
emit RendererSet(newRenderer);
}
/// @notice Pending ETH claimable for a single Share (tokenId), in wei: (accEthPerShare − feeDebt) de-scaled.
/// Each NFT is exactly one Share, so there is no per-holder share count. Returns 0 for a
/// burned/never-minted id (its feeDebt is meaningless without a live owner). This is the exact amount
/// `settleClaim` pays; UIs aggregate it across a wallet's tokenIds.
function pendingFees(uint256 tokenId) public view returns (uint256) {
if (_ownerOf(tokenId) == address(0)) return 0;
uint256 debt = feeDebt[tokenId];
uint256 acc = accEthPerShare;
return acc > debt ? (acc - debt) / ACC_SCALE : 0;
}
/// @notice ClaimRouter-only. Settle the pending ETH of `tokenIds` (each MUST be owned by `holder`), snapshot
/// their feeDebt up to accEthPerShare, and forward the aggregate ETH to the router. CEI: every
/// feeDebt write (effect) precedes the single ETH send (interaction); `nonReentrant`. A duplicate id
/// in the array pays once (the second sighting sees debt == acc → adds 0), so double-claim is
/// impossible. Reverts atomically if any id is not `holder`'s — a failed claim never loses pending.
/// The router turns this ETH into the holder's chosen dividend (ETH via `claim`, a stock via
/// `claimInto`); the core itself never touches a stock token.
function settleClaim(uint256[] calldata tokenIds, address holder) external nonReentrant returns (uint256 ethOut) {
if (msg.sender != claimRouter) revert ClaimRouterOnly();
uint256 acc = accEthPerShare;
uint256 n = tokenIds.length;
for (uint256 i; i < n; ++i) {
uint256 id = tokenIds[i];
if (_ownerOf(id) != holder) revert NotShareOwner();
uint256 debt = feeDebt[id];
if (acc > debt) {
unchecked {
ethOut += (acc - debt) / ACC_SCALE;
}
feeDebt[id] = acc; // effect BEFORE the interaction (CEI)
}
}
if (ethOut > 0) {
emit FeesSettled(holder, ethOut, n);
_sendEth(claimRouter, ethOut); // interaction
}
}
/* ERC20 → NFT SYNC */
function _afterTokenTransfer(address from, address to, uint256) internal override {
if (_isSilent()) return;
// The v3 pool is the reserve holder (KALEIDO flows core→pool at seed, pool→buyer on a buy, pool→core on a
// fee collect) — exclude it from Share accounting, exactly as the v4 PoolManager was excluded. `pool` is 0
// until seeded, which is harmless (no transfer references it before then).
address p = pool;
address self = address(this);
bool fromIsUser = from != address(0) && from != p && from != self;
bool toIsUser = to != address(0) && to != p && to != self;
if (fromIsUser && toIsUser) _realignPair(from, to);
else if (fromIsUser) _realignSolo(from);
else if (toIsUser) _realignSolo(to);
}
function _realignPair(address from, address to) private {
uint256 fromTarget = balanceOf(from) / UNIT;
uint256 toTarget = balanceOf(to) / UNIT;
uint256 fromCur = _ownedLength(from);
uint256 toCur = _ownedLength(to);
uint256 fromLoses = fromCur > fromTarget ? fromCur - fromTarget : 0;
uint256 toGains = toTarget > toCur ? toTarget - toCur : 0;
uint256 transferable = fromLoses < toGains ? fromLoses : toGains;
uint256 toBurn = fromLoses - transferable;
uint256 toMint = toGains - transferable;
for (uint256 i = 0; i < transferable; i++) {
uint256 tokenId = _pickTail(from);
_move(from, to, tokenId);
}
for (uint256 i = 0; i < toBurn; i++) {
uint256 tokenId = _pickTail(from);
_burnNFT(from, tokenId);
}
for (uint256 i = 0; i < toMint; i++) {
_mintNFT(to);
}
}
function _realignSolo(address user) private {
uint256 target = balanceOf(user) / UNIT;
uint256 cur = _ownedLength(user);
if (target > cur) {
uint256 toMint = target - cur;
for (uint256 i = 0; i < toMint; i++) {
_mintNFT(user);
}
} else if (cur > target) {
uint256 toBurn = cur - target;
for (uint256 i = 0; i < toBurn; i++) {
uint256 tokenId = _pickTail(user);
_burnNFT(user, tokenId);
}
}
}
/* NFT INTERNAL OPS */
function _mintNFT(address to) private {
unchecked {
uint256 tokenId = ++_nextTokenId;
uint32 index = _ownedLength(to);
_setOo(tokenId, to, index);
// forge-lint: disable-next-line(unsafe-typecast) — tokenId is bounded (DN404 uint32 id packing).
_ownedSet(to, index, uint32(tokenId));
_setOwnedLength(to, index + 1);
mintedAt[tokenId] = block.timestamp;
feeDebt[tokenId] = accEthPerShare;
totalShares = totalShares + 1;
mirror.emitTransfer(address(0), to, tokenId);
emit NFTMinted(to, tokenId);
}
}
function _burnNFT(address from, uint256 tokenId) private {
uint32 index = _ownedIndexOf(tokenId);
uint32 lastIdx = _ownedLength(from) - 1;
if (index != lastIdx) {
uint32 lastTokenId = _ownedGet(from, lastIdx);
_ownedSet(from, index, lastTokenId);
_setOo(lastTokenId, from, index);
}
_setOwnedLength(from, lastIdx);
delete _oo[tokenId];
delete _nftApprovals[tokenId];
delete mintedAt[tokenId];
// v1.1 forfeiture (VEG2-1126): the burned Share's unclaimed pending (its accrued dividend, already ETH in
// the contract) is redistributed to the SURVIVORS. Credit accEthPerShare INLINE over the survivor count
// AFTER this burn, so the forfeit lands on exactly the Shares live at the burn instant — no window for a
// Share minted afterwards to capture it (this is what keeps "JIT impossible by construction" true; a lazy
// park-then-sweep let an attacker mint between burn and sweep and dilute the honest survivors). O(1),
// accounting-only, no external call, no loop → a burn can never brick a transfer (invariant #5). feeDebt
// is dropped so the same pending can never ALSO be claimed (burned ids fail settleClaim's owner check
// anyway). Only when the LAST Share burns (no survivors) do we PARK the amount in forfeitedWei —
// sweepForfeits redistributes that once a Share exists again, dodging a divide-by-zero here.
uint256 acc = accEthPerShare;
uint256 debt = feeDebt[tokenId];
delete feeDebt[tokenId];
unchecked {
totalShares = totalShares - 1;
}
if (acc > debt) {
uint256 forfeitScaled = acc - debt;
if (totalShares > 0) {
accEthPerShare = acc + forfeitScaled / totalShares; // survivors inherit, atomically
} else {
forfeitedWei += forfeitScaled / ACC_SCALE; // last Share out — park until one returns
}
uint256 forfeitWei = forfeitScaled / ACC_SCALE;
if (forfeitWei > 0) emit Forfeited(tokenId, forfeitWei);
}
mirror.emitTransfer(from, address(0), tokenId);
emit NFTBurned(from, tokenId);
}
function _move(address from, address to, uint256 tokenId) private {
uint32 fromIdx = _ownedIndexOf(tokenId);
uint32 lastIdx = _ownedLength(from) - 1;
if (fromIdx != lastIdx) {
uint32 lastTokenId = _ownedGet(from, lastIdx);
_ownedSet(from, fromIdx, lastTokenId);
_setOo(lastTokenId, from, fromIdx);
}
_setOwnedLength(from, lastIdx);
uint32 toIdx = _ownedLength(to);
// forge-lint: disable-next-line(unsafe-typecast) — tokenId is bounded (DN404 uint32 id packing).
_ownedSet(to, toIdx, uint32(tokenId));
_setOo(tokenId, to, toIdx);
_setOwnedLength(to, toIdx + 1);
// stats/provenance: mintedAt tracks the current holder's acquisition time (v1.1 — no eligibility use).
mintedAt[tokenId] = block.timestamp;
delete _nftApprovals[tokenId];
mirror.emitTransfer(from, to, tokenId);
}
function _pickTail(address user) private view returns (uint256) {
return _ownedGet(user, _ownedLength(user) - 1);
}
/* MIRROR CALLBACKS */
modifier onlyMirror() {
if (msg.sender != address(mirror)) revert MirrorOnly();
_;
}
function handleNFTTransfer(address from, address to, uint256 tokenId, address caller)
external
onlyMirror
nonReentrant
{
if (to == address(0)) revert TransferToZero();
// The core reserve and canonical pool are deliberately excluded from Share accounting in the ERC-20 hook.
// The mirror path runs its paired KALEIDO transfer under `_SILENT`, so allowing either destination here
// would bypass that exclusion: the excluded address would retain a live Share while a later pool/core
// outflow could mint another one. Reject before `_move` so tokenId, feeDebt and ownership remain unchanged.
if (to == address(this) || to == pool) revert ExcludedShareRecipient();
if (from == to) revert SelfTransferDisallowed();
address owner_ = _ownerOf(tokenId);
if (owner_ == address(0) || owner_ != from) revert InvalidTokenId();
if (caller != owner_ && _nftApprovals[tokenId] != caller && !_nftOperatorApprovals[owner_][caller]) {
revert NotOwnerOrApproved();
}
_move(from, to, tokenId);
_setSilent(true);
_transfer(from, to, UNIT);
_setSilent(false);
}
function handleNFTApprove(address spender, uint256 tokenId, address caller) external onlyMirror {
address owner_ = _ownerOf(tokenId);
if (owner_ == address(0)) revert InvalidTokenId();
if (caller != owner_ && !_nftOperatorApprovals[owner_][caller]) revert NotOwnerOrApproved();
_nftApprovals[tokenId] = spender;
mirror.emitApproval(owner_, spender, tokenId);
}
function handleNFTSetApprovalForAll(address operator, bool approved, address caller) external onlyMirror {
_nftOperatorApprovals[caller][operator] = approved;
mirror.emitApprovalForAll(caller, operator, approved);
}
/* NFT VIEWS */
function nftOwnerOf(uint256 tokenId) external view returns (address o) {
o = _ownerOf(tokenId);
if (o == address(0)) revert InvalidTokenId();
}
function nftBalanceOf(address owner_) external view returns (uint256) {
return _ownedLength(owner_);
}
function nftGetApproved(uint256 tokenId) external view returns (address) {
return _nftApprovals[tokenId];
}
function nftIsApprovedForAll(address o, address s) external view returns (bool) {
return _nftOperatorApprovals[o][s];
}
/// @notice ERC-721 tokenURI for a Share, via the replaceable `renderer` (VEG2-1129). Reverts only for a
/// non-existent id (ERC-721 requirement). With no renderer set it returns "" (the pre-renderer
/// default); with one set it forwards, but a reverting/gas-heavy renderer degrades to "" — a bad or
/// malicious renderer can never brick marketplace metadata. View-only: on no money or transfer path.
function nftTokenURI(uint256 tokenId) external view returns (string memory) {
if (_ownerOf(tokenId) == address(0)) revert InvalidTokenId();
address r = renderer;
if (r == address(0)) return "";
try IKaleidoRenderer(r).tokenURI(tokenId) returns (string memory uri) {
return uri;
} catch {
return "";
}
}
/// @notice ERC-7572 collection metadata, via the replaceable `renderer`. Same degrade-to-"" safety as
/// nftTokenURI. Surfaced on the mirror as `contractURI()`.
function nftContractURI() external view returns (string memory) {
address r = renderer;
if (r == address(0)) return "";
try IKaleidoRenderer(r).contractURI() returns (string memory uri) {
return uri;
} catch {
return "";
}
}
function ownedTokensOf(address owner_) external view returns (uint256[] memory ids) {
uint256 n = _ownedLength(owner_);
ids = new uint256[](n);
for (uint256 i = 0; i < n; i++) {
ids[i] = uint256(_ownedGet(owner_, i));
}
}
/* SILENT FLAG */
function _setSilent(bool v) private {
assembly {
tstore(_SILENT_SLOT, v)
}
}
function _isSilent() private view returns (bool v) {
assembly {
v := tload(_SILENT_SLOT)
}
}
/// @dev Accept the v3 position NFT if the NonfungiblePositionManager ever safe-mints it to us (current v3
/// mints via plain _mint, but this keeps `seed` robust across NPM variants). Not an ERC-721 itself.
function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {
return this.onERC721Received.selector;
}
receive() external payable { }
}
Chain explorer2174msChain node73ms