SlvrVoteEscrow

0xd9b8fbd61033145c5496132153ce675756313b71

Verification
Verified
v0.8.33+commit.64118f21
Type
Contract
16,567 bytes
ABI entries
110
39 read · 20 write
License
none

Contract information

Address
0xd9b8fbd61033145c5496132153ce675756313b71
Chain
Robinhood Chain (4663)
Compiler
v0.8.33+commit.64118f21
Optimization
Enabled
Creation tx
0x0111cc2a33…24bdab516c

Token

Name
Slvr Vote Escrow
Symbol
veSLVR
Decimals
Holders
782

Read contract (39)

CLOCK_MODE()string
MMAX()uint256
P()uint256
SLVR()address
TMAX()uint256
approve(address, uint256)
authorizedContract()address
balanceOf(address)uint256
clock()uint48
delegate(address)
delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32)
delegates(address)address
getApproved(uint256)address
getLock(uint256)tuple
getMaxLockTokenId(address)uint256
getPastTotalSupply(uint256)uint256
getPastVotes(address, uint256)uint256
getPermanentLockTokenId(address)uint256
getRewardMultiplier(uint256)uint256
getStakingWeight(uint256)uint256
getTotalVotingPower(address)uint256
getUserTokens(address)uint256[]
getVotes(address)uint256
getVotingPower(uint256)uint256
isApprovedForAll(address, address)bool
locks(uint256)uint256, uint256, uint256, bool, bool
metadataContract()address
name()string
owner()address
ownerOf(uint256)address
pendingOwner()address
setApprovalForAll(address, bool)
stakingContract()address
supportsInterface(bytes4)bool
symbol()string
tokenIndexInUserList(uint256)uint256
tokenURI(uint256)string
userPermanentLockId(address)uint256
userTokens(address, uint256)uint256

Events (14)

ApprovalApprovalForAllDelegateChangedDelegateVotesChangedLockConvertedToPermanentLockCreatedLockExtendedLockIncreasedLockWithdrawnMetadataContractUpdatedOwnershipTransferStartedOwnershipTransferredStakingContractUpdatedTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "slvr_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyPermanent",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DelegationNotSupported",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DurationExceedsMax",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "timepoint",
        "type": "uint256"
      },
      {
        "internalType": "uint48",
        "name": "clock_",
        "type": "uint48"
      }
    ],
    "name": "ERC5805FutureLookup",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721IncorrectOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721InsufficientApproval",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOperator",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC721InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ERC721NonexistentToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidDuration",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockEmpty",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockEndExceedsMax",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockNotExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockTypeMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NonTransferable",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotAuthorized",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotOwner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPermanentLock",
    "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": "PermanentLock",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PermanentLockExists",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "bits",
        "type": "uint8"
      },
      {
        "internalType": "int256",
        "name": "value",
        "type": "int256"
      }
    ],
    "name": "SafeCastOverflowedIntDowncast",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint8",
        "name": "bits",
        "type": "uint8"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "SafeCastOverflowedUintDowncast",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SoulboundNFT",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenDoesNotExist",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenNotInList",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "expiry",
        "type": "uint256"
      }
    ],
    "name": "VotesExpiredSignature",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroReceived",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "approved",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "operator",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "approved",
        "type": "bool"
      }
    ],
    "name": "ApprovalForAll",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "delegator",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "fromDelegate",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "toDelegate",
        "type": "address"
      }
    ],
    "name": "DelegateChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "delegate",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "previousVotes",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newVotes",
        "type": "uint256"
      }
    ],
    "name": "DelegateVotesChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "permanentTokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "LockConvertedToPermanent",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "duration",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "permanent",
        "type": "bool"
      }
    ],
    "name": "LockCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newLockEnd",
        "type": "uint256"
      }
    ],
    "name": "LockExtended",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "addedAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newLockEnd",
        "type": "uint256"
      }
    ],
    "name": "LockIncreased",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "LockWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldContract",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newContract",
        "type": "address"
      }
    ],
    "name": "MetadataContractUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "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": "oldContract",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newContract",
        "type": "address"
      }
    ],
    "name": "StakingContractUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "CLOCK_MODE",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MMAX",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "P",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "SLVR",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TMAX",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "permanent",
        "type": "bool"
      }
    ],
    "name": "addToMaxLock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "authorizedContract",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "clock",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "convertToPermanentLock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "duration",
        "type": "uint256"
      }
    ],
    "name": "createLock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "duration",
        "type": "uint256"
      }
    ],
    "name": "createLockFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "createPermanentLock",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "createPermanentLockFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "delegatee",
        "type": "address"
      }
    ],
    "name": "delegate",
    "outputs": [],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      },
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      },
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "delegateBySig",
    "outputs": [],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "delegates",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "duration",
        "type": "uint256"
      }
    ],
    "name": "extendLock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getApproved",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getLock",
    "outputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "amount",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "lockStart",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "lockEnd",
            "type": "uint256"
          },
          {
            "internalType": "bool",
            "name": "permanent",
            "type": "bool"
          },
          {
            "internalType": "bool",
            "name": "isMaxTime",
            "type": "bool"
          }
        ],
        "internalType": "struct SlvrVoteEscrow.Lock",
        "name": "",
        "type": "tuple"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getMaxLockTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "timepoint",
        "type": "uint256"
      }
    ],
    "name": "getPastTotalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "timepoint",
        "type": "uint256"
      }
    ],
    "name": "getPastVotes",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getPermanentLockTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getRewardMultiplier",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "m",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getStakingWeight",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "w",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getTotalVotingPower",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "totalVe",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      }
    ],
    "name": "getUserTokens",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "getVotes",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "getVotingPower",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ve",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "newDuration",
        "type": "uint256"
      }
    ],
    "name": "increaseLock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "newDuration",
        "type": "uint256"
      }
    ],
    "name": "increaseLockFor",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "increasePermanentLock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "operator",
        "type": "address"
      }
    ],
    "name": "isApprovedForAll",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "locks",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "lockStart",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "lockEnd",
        "type": "uint256"
      },
      {
        "internalType": "bool",
        "name": "permanent",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "isMaxTime",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "metadataContract",
    "outputs": [
      {
        "internalType": "contract ISlvrVoteEscrowMetadata",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "ownerOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "safeTransferFrom",
    "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": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "safeTransferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "name": "setApprovalForAll",
    "outputs": [],
    "stateMutability": "pure",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "contract_",
        "type": "address"
      }
    ],
    "name": "setAuthorizedContract",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_metadataContract",
        "type": "address"
      }
    ],
    "name": "setMetadataContract",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_stakingContract",
        "type": "address"
      }
    ],
    "name": "setStakingContract",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stakingContract",
    "outputs": [
      {
        "internalType": "contract ISlvrVoteEscrowStaking",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes4",
        "name": "interfaceId",
        "type": "bytes4"
      }
    ],
    "name": "supportsInterface",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tokenIndexInUserList",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "tokenURI",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userPermanentLockId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "userTokens",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {IERC5805} from "@openzeppelin/contracts/interfaces/IERC5805.sol";
import {ISlvrVoteEscrowMetadata} from "./interfaces/ISlvrVoteEscrowMetadata.sol";
import {ISlvrVoteEscrowStaking} from "./interfaces/ISlvrVoteEscrowStaking.sol";
import {VoteCheckpointLib} from "./libraries/VoteCheckpointLib.sol";

/// @title SlvrVoteEscrow
/// @notice Vote escrow NFT contract for locked SLVR staking with time-based voting power and reward multipliers
/// @dev Implements an ERC-5805 voting surface (timestamp clock) with checkpointed history so an
///      on-chain Governor can read past voting power (`getPastVotes` / `getPastTotalSupply`).
///      Checkpoints reproduce the decaying `getVotingPower` curve exactly, except that a lock's
///      governance power ends at the last day boundary at or before its `lockEnd` (its final
///      sub-day tail — where power is near zero anyway — carries no votes). The NFT is soulbound,
///      so votes are fixed to the holder: `delegates(a) == a` and delegation elsewhere reverts.
contract SlvrVoteEscrow is ERC721, Ownable2Step, ReentrancyGuard, IERC5805 {
    using VoteCheckpointLib for VoteCheckpointLib.History;
    using SafeERC20 for IERC20;
    uint256 public constant TMAX = 4 * 30 days; // 4 months max lock
    uint256 public constant MMAX = 2.5e18; // 2.5x max multiplier for normal locks
    uint256 public constant P = 2.0e18; // Permanent factor (multiplier = 1 + (MMAX-1)*P/1e18 = 4.0x)

    // Custom errors
    error ZeroAddress();
    error ZeroAmount();
    error ZeroReceived();
    error InvalidDuration();
    error DurationExceedsMax();
    error LockEndExceedsMax();
    error NotAuthorized();
    error NotOwner();
    error TokenDoesNotExist();
    error PermanentLockExists();
    error LockTypeMismatch();
    error PermanentLock();
    error NotPermanentLock();
    error AlreadyPermanent();
    error LockEmpty();
    error LockNotExpired();
    error TokenNotInList();
    error NonTransferable();
    error SoulboundNFT();
    error ERC5805FutureLookup(uint256 timepoint, uint48 clock_);
    error DelegationNotSupported();

    IERC20 public immutable SLVR;

    /// @notice The metadata contract address (upgradeable)
    ISlvrVoteEscrowMetadata public metadataContract;

    /// @notice The staking contract address (optional, for cleanup on burn)
    ISlvrVoteEscrowStaking public stakingContract;

    uint256 private _tokenIdCounter;

    struct Lock {
        uint256 amount; // Locked SLVR amount
        uint256 lockStart; // Lock start timestamp
        uint256 lockEnd; // Lock expiration timestamp (0 for permanent)
        bool permanent; // True if permanent lock (non-withdrawable)
        bool isMaxTime; // True if this is a TMAX duration lock (lockEnd - lockStart == TMAX)
    }

    mapping(uint256 => Lock) public locks;
    mapping(address => uint256[]) public userTokens; // User's token IDs
    mapping(uint256 => uint256) public tokenIndexInUserList; // tokenId => index+1 (0 means not in list)

    // Explicit tracking of permanent locks (only one per user allowed)
    mapping(address => uint256) public userPermanentLockId; // User's permanent lock token ID (0 if none)

    // Authorized contract that can create locks on behalf of users (e.g., lottery)
    address public authorizedContract;

    // Governance vote checkpoints (ERC-5805): per-account and global (bias, slope) histories,
    // updated on every lock mutation so past voting power is exactly reproducible.
    mapping(address => VoteCheckpointLib.History) private _accountVotes;
    VoteCheckpointLib.History private _totalVotes;

    event LockCreated(uint256 indexed tokenId, address indexed user, uint256 amount, uint256 duration, bool permanent);
    event LockIncreased(uint256 indexed tokenId, uint256 addedAmount, uint256 newLockEnd);
    event LockExtended(uint256 indexed tokenId, uint256 newLockEnd);
    event LockWithdrawn(uint256 indexed tokenId, address indexed user, uint256 amount);
    event LockConvertedToPermanent(uint256 indexed tokenId, uint256 indexed permanentTokenId, uint256 amount);
    event MetadataContractUpdated(address indexed oldContract, address indexed newContract);
    event StakingContractUpdated(address indexed oldContract, address indexed newContract);

    constructor(address slvr_, address owner_) ERC721("Slvr Vote Escrow", "veSLVR") Ownable(owner_) {
        if (slvr_ == address(0)) revert ZeroAddress();
        SLVR = IERC20(slvr_);
        _tokenIdCounter = 1; // Start at 1 so tokenId 0 can be used as "no token" sentinel value
    }

    /// @notice Internal helper to measure actual received SLVR amount (handles fee-on-transfer)
    /// @param amount Expected amount to receive
    /// @return received Actual amount received
    function _measureReceived(uint256 amount) private returns (uint256 received) {
        uint256 beforeBal = SLVR.balanceOf(address(this));
        SLVR.safeTransferFrom(msg.sender, address(this), amount);
        uint256 afterBal = SLVR.balanceOf(address(this));
        received = afterBal - beforeBal;
        if (received == 0) revert ZeroReceived();
    }

    /// @notice Create a normal time-based lock
    /// @param amount Amount of SLVR to lock
    /// @param duration Lock duration in seconds (must be <= TMAX, except for permanent locks)
    function createLock(uint256 amount, uint256 duration) external nonReentrant returns (uint256 tokenId) {
        if (amount == 0) revert ZeroAmount();
        if (duration == 0 || duration > TMAX) revert InvalidDuration();

        // Users can create multiple TMAX locks - no restriction here

        uint256 received = _measureReceived(amount);

        tokenId = _tokenIdCounter++;
        uint256 lockEnd = block.timestamp + duration;
        
        // Explicit safety check: ensure lockEnd never exceeds TMAX from now (except for permanent locks)
        if (lockEnd > block.timestamp + TMAX) revert LockEndExceedsMax();

        locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: lockEnd, permanent: false, isMaxTime: duration == TMAX});
        _votesCreated(msg.sender, tokenId);

        _addTokenToUser(msg.sender, tokenId);
        _mint(msg.sender, tokenId);

        // Optionally stake in staking contract if set
        _autoStakeIfEnabled(tokenId);

        emit LockCreated(tokenId, msg.sender, received, duration, false);
    }

    /// @notice Internal helper to measure actual received SLVR amount from authorized contract (handles fee-on-transfer)
    /// @param amount Expected amount to receive
    /// @return received Actual amount received
    function _measureReceivedFrom(address from, uint256 amount) private returns (uint256 received) {
        uint256 beforeBal = SLVR.balanceOf(address(this));
        SLVR.safeTransferFrom(from, address(this), amount);
        uint256 afterBal = SLVR.balanceOf(address(this));
        received = afterBal - beforeBal;
        if (received == 0) revert ZeroReceived();
    }

    /// @notice Create a lock on behalf of a user (only authorized contract)
    /// @param user The user to create the lock for
    /// @param amount Amount of SLVR to lock
    /// @param duration Lock duration in seconds (must be <= TMAX, except for permanent locks)
    function createLockFor(address user, uint256 amount, uint256 duration) external nonReentrant returns (uint256 tokenId) {
        if (msg.sender != authorizedContract) revert NotAuthorized();
        if (user == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (duration == 0 || duration > TMAX) revert InvalidDuration();

        // Users can create multiple TMAX locks - no restriction here

        uint256 received = _measureReceivedFrom(msg.sender, amount);

        tokenId = _tokenIdCounter++;
        uint256 lockEnd = block.timestamp + duration;
        
        // Explicit safety check: ensure lockEnd never exceeds TMAX from now (except for permanent locks)
        if (lockEnd > block.timestamp + TMAX) revert LockEndExceedsMax();

        locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: lockEnd, permanent: false, isMaxTime: duration == TMAX});
        _votesCreated(user, tokenId);

        _addTokenToUser(user, tokenId);
        _mint(user, tokenId);

        // Optionally stake in staking contract if set
        _autoStakeIfEnabled(tokenId);

        emit LockCreated(tokenId, user, received, duration, false);
    }

    /// @notice Create a permanent lock on behalf of a user (only authorized contract)
    /// @param user The user to create the lock for
    /// @param amount Amount of SLVR to permanently lock and burn
    function createPermanentLockFor(address user, uint256 amount) external nonReentrant returns (uint256 tokenId) {
        if (msg.sender != authorizedContract) revert NotAuthorized();
        if (user == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();

        // Prevent creating multiple permanent locks - user can only have one permanent lock
        if (userPermanentLockId[user] != 0) revert PermanentLockExists();

        uint256 received = _measureReceivedFrom(msg.sender, amount);

        // Actually burn the tokens to reduce total supply (burn received amount, not expected)
        ERC20Burnable(address(SLVR)).burn(received);

        tokenId = _tokenIdCounter++;

        locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: 0, permanent: true, isMaxTime: false});
        _votesCreated(user, tokenId);

        _addTokenToUser(user, tokenId);
        _mint(user, tokenId);
        userPermanentLockId[user] = tokenId;

        // Optionally stake in staking contract if set
        _autoStakeIfEnabled(tokenId);

        emit LockCreated(tokenId, user, received, 0, true);
    }

    /// @notice Add to max lock or create one if it doesn't exist (only authorized contract)
    /// @param user The user to add to/create max lock for
    /// @param amount Amount of SLVR to lock
    /// @param permanent If true, create/add to permanent lock (burn). If false, create/add to TMAX lock.
    /// @return tokenId The token ID of the max lock (existing or newly created)
    function addToMaxLock(address user, uint256 amount, bool permanent) external nonReentrant returns (uint256 tokenId) {
        if (msg.sender != authorizedContract) revert NotAuthorized();
        if (user == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();

        if (permanent) {
            // For permanent locks, check for existing permanent lock only
            tokenId = userPermanentLockId[user];
        } else {
            // For TMAX locks, if user has permanent lock, they can't add to TMAX lock
            if (userPermanentLockId[user] != 0) revert PermanentLockExists();
            // Users can have multiple TMAX locks, so we'll create a new one each time
            tokenId = 0;
        }

        if (tokenId == 0) {
            // User doesn't have the requested lock type, create one
            if (permanent) {
                uint256 received = _measureReceivedFrom(msg.sender, amount);
                ERC20Burnable(address(SLVR)).burn(received);
                tokenId = _tokenIdCounter++;
                locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: 0, permanent: true, isMaxTime: false});
                _votesCreated(user, tokenId);
                _addTokenToUser(user, tokenId);
                _mint(user, tokenId);
                userPermanentLockId[user] = tokenId;
                // Optionally stake in staking contract if set
                _autoStakeIfEnabled(tokenId);
                emit LockCreated(tokenId, user, received, 0, true);
            } else {
                uint256 received = _measureReceivedFrom(msg.sender, amount);
                tokenId = _tokenIdCounter++;
                locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: block.timestamp + TMAX, permanent: false, isMaxTime: true});
                _votesCreated(user, tokenId);
                _addTokenToUser(user, tokenId);
                _mint(user, tokenId);
                // Optionally stake in staking contract if set
                _autoStakeIfEnabled(tokenId);
                emit LockCreated(tokenId, user, received, TMAX, false);
            }
        } else {
            // User has existing max lock, add to it
            Lock storage lock = locks[tokenId];
            if (lock.permanent != permanent) revert LockTypeMismatch();
            Lock memory prev = lock;

            uint256 received = _measureReceivedFrom(msg.sender, amount);
            
            if (permanent) {
                ERC20Burnable(address(SLVR)).burn(received);
            }
            
            lock.amount += received;
            
            if (!permanent) {
                // Extend TMAX lock to full duration from now
                lock.lockEnd = block.timestamp + TMAX;
                lock.lockStart = block.timestamp;
                lock.isMaxTime = true;
            }

            _moveVotes(user, prev, lock);

            // Weight increased (amount up, TMAX re-maxed) — checkpoint so the staking contract tracks
            // the new weight instead of leaving the holder earning on a stale, lower balance.
            _autoCheckpointIfEnabled(tokenId);

            emit LockIncreased(tokenId, received, lock.lockEnd);
        }

        return tokenId;
    }

    /// @notice Set authorized contract address (owner only)
    /// @param contract_ The contract address that can create locks on behalf of users
    function setAuthorizedContract(address contract_) external onlyOwner {
        authorizedContract = contract_;
    }

    /// @notice Create a permanent lock (non-withdrawable burn)
    /// @param amount Amount of SLVR to permanently lock and burn
    function createPermanentLock(uint256 amount) external nonReentrant returns (uint256 tokenId) {
        if (amount == 0) revert ZeroAmount();

        // Prevent creating multiple permanent locks - user can only have one permanent lock
        if (userPermanentLockId[msg.sender] != 0) revert PermanentLockExists();

        uint256 received = _measureReceived(amount);

        // Actually burn the tokens to reduce total supply (burn received amount, not expected)
        ERC20Burnable(address(SLVR)).burn(received);

        tokenId = _tokenIdCounter++;

        locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: 0, permanent: true, isMaxTime: false});
        _votesCreated(msg.sender, tokenId);

        _addTokenToUser(msg.sender, tokenId);
        _mint(msg.sender, tokenId);
        userPermanentLockId[msg.sender] = tokenId;

        // Optionally stake in staking contract if set
        _autoStakeIfEnabled(tokenId);

        emit LockCreated(tokenId, msg.sender, received, 0, true);
    }

    /// @notice Increase lock amount and optionally extend duration
    /// @param tokenId Token ID of the lock
    /// @param amount Additional SLVR to add (must be > 0)
    /// @param newDuration New lock duration:
    ///                   - 0 = lock new tokens for remainder of existing lock (no extension)
    ///                   - TMAX = extend to max duration (4 months) from now
    ///                   - Any other value = add that duration to current expiration time
    /// @dev You can always increase the amount in a lock, and optionally extend the duration
    /// @dev When newDuration == 0, new tokens are locked until the existing lock's expiration
    /// @dev When extending, if newDuration == TMAX, the lock is extended to max duration from now
    /// @dev Otherwise, the duration is added to the current expiration time
    /// @dev When newDuration > 0, this resets lockStart to block.timestamp, which changes
    ///      the lock duration and causes staking weight to "snap" to a new value. The staking
    ///      contract handles this via checkpointing.
    function increaseLock(uint256 tokenId, uint256 amount, uint256 newDuration) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();
        if (amount == 0) revert ZeroAmount();

        Lock storage lock = locks[tokenId];
        if (lock.permanent) revert PermanentLock();
        Lock memory prev = lock;

        uint256 received = _measureReceived(amount);

        lock.amount += received;

        if (newDuration > 0) {
            require(newDuration <= TMAX, "duration must be <= TMAX");
            uint256 proposedEnd;
            if (newDuration == TMAX) {
                // Extend to max duration from now (reset to full 4 months)
                proposedEnd = block.timestamp + TMAX;
            } else {
                // Add duration to current expiration time
                uint256 currentEnd = lock.lockEnd > block.timestamp ? lock.lockEnd : block.timestamp;
                proposedEnd = currentEnd + newDuration;
            }
            // Explicit safety check: ensure lockEnd never exceeds TMAX from now (except for permanent locks)
            require(proposedEnd <= block.timestamp + TMAX, "lockEnd exceeds TMAX");
            
            lock.lockEnd = proposedEnd;
            lock.lockStart = block.timestamp; // Reset start time for new duration
            lock.isMaxTime = (proposedEnd == block.timestamp + TMAX);
        }

        _moveVotes(msg.sender, prev, lock);

        // Optionally checkpoint in staking contract if set (weight may have changed)
        _autoCheckpointIfEnabled(tokenId);

        emit LockIncreased(tokenId, received, lock.lockEnd);
    }

    /// @notice Increase lock amount and optionally extend duration (only authorized contract)
    /// @param tokenId Token ID of the lock
    /// @param amount Additional SLVR to add (must be > 0)
    /// @param newDuration New lock duration (see increaseLock for details)
    function increaseLockFor(uint256 tokenId, uint256 amount, uint256 newDuration) external nonReentrant {
        if (msg.sender != authorizedContract) revert NotAuthorized();
        if (ownerOf(tokenId) == address(0)) revert TokenDoesNotExist();
        if (amount == 0) revert ZeroAmount();

        Lock storage lock = locks[tokenId];
        if (lock.permanent) revert PermanentLock();
        Lock memory prev = lock;

        uint256 received = _measureReceivedFrom(msg.sender, amount);

        lock.amount += received;

        if (newDuration > 0) {
            if (newDuration > TMAX) revert DurationExceedsMax();
            uint256 proposedEnd;
            if (newDuration == TMAX) {
                // Extend to max duration from now (reset to full 4 months)
                proposedEnd = block.timestamp + TMAX;
            } else {
                // Add duration to current expiration time
                uint256 currentEnd = lock.lockEnd > block.timestamp ? lock.lockEnd : block.timestamp;
                proposedEnd = currentEnd + newDuration;
            }
            // Explicit safety check: ensure lockEnd never exceeds TMAX from now (except for permanent locks)
            if (proposedEnd > block.timestamp + TMAX) revert LockEndExceedsMax();
            
            lock.lockEnd = proposedEnd;
            lock.lockStart = block.timestamp; // Reset start time for new duration
            lock.isMaxTime = (proposedEnd == block.timestamp + TMAX);
        }

        _moveVotes(ownerOf(tokenId), prev, lock);

        // Optionally checkpoint in staking contract if set (weight may have changed)
        _autoCheckpointIfEnabled(tokenId);

        emit LockIncreased(tokenId, received, lock.lockEnd);
    }

    /// @notice Increase permanent lock amount by burning additional tokens
    /// @param tokenId Token ID of the permanent lock
    /// @param amount Additional SLVR to permanently lock and burn
    function increasePermanentLock(uint256 tokenId, uint256 amount) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();
        if (amount == 0) revert ZeroAmount();

        Lock storage lock = locks[tokenId];
        if (!lock.permanent) revert NotPermanentLock();
        Lock memory prev = lock;

        uint256 received = _measureReceived(amount);

        // Burn the additional tokens (burn received amount, not expected)
        ERC20Burnable(address(SLVR)).burn(received);

        // Increase the lock amount
        lock.amount += received;

        _moveVotes(msg.sender, prev, lock);

        // Optionally checkpoint in staking contract if set (weight has increased)
        _autoCheckpointIfEnabled(tokenId);

        emit LockIncreased(tokenId, received, 0);
    }

    /// @notice Extend lock duration (relock to reset multiplier)
    /// @param tokenId Token ID of the lock
    /// @param duration New lock duration (must be <= TMAX, except for permanent locks)
    /// @dev Resets lockStart to block.timestamp, which changes the lock duration and
    ///      causes staking weight to "snap" to a new value based on the new duration.
    ///      The staking contract handles this via checkpointing.
    function extendLock(uint256 tokenId, uint256 duration) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();
        if (duration == 0 || duration > TMAX) revert InvalidDuration();

        Lock storage lock = locks[tokenId];
        if (lock.permanent) revert PermanentLock();
        Lock memory prev = lock;

        uint256 proposedEnd;
        if (duration == TMAX) {
            proposedEnd = block.timestamp + TMAX;
        } else {
            uint256 currentEnd = lock.lockEnd > block.timestamp ? lock.lockEnd : block.timestamp;
            proposedEnd = currentEnd + duration;
        }
        // Explicit safety check: ensure lockEnd never exceeds TMAX from now (except for permanent locks)
        if (proposedEnd > block.timestamp + TMAX) revert LockEndExceedsMax();

        lock.lockEnd = proposedEnd;
        lock.lockStart = block.timestamp; // Reset start time
        lock.isMaxTime = (proposedEnd == block.timestamp + TMAX);

        _moveVotes(msg.sender, prev, lock);

        // Optionally checkpoint in staking contract if set (weight may have changed)
        _autoCheckpointIfEnabled(tokenId);

        emit LockExtended(tokenId, lock.lockEnd);
    }

    /// @notice Convert a normal lock to a permanent lock by burning the NFT and depositing tokens to permanent lock
    /// @param tokenId Token ID of the normal lock to convert
    /// @dev Burns the normal lock NFT and transfers its tokens to the user's permanent lock (creates one if needed)
    function convertToPermanentLock(uint256 tokenId) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();

        Lock storage lock = locks[tokenId];
        if (lock.permanent) revert AlreadyPermanent();
        Lock memory prevTimeLock = lock;

        uint256 amount = lock.amount;
        if (amount == 0) revert LockEmpty();

        // Get or create permanent lock
        uint256 permanentTokenId = userPermanentLockId[msg.sender];
        
        if (permanentTokenId == 0) {
            // Create permanent lock
            permanentTokenId = _tokenIdCounter++;
            locks[permanentTokenId] = Lock({
                amount: amount,
                lockStart: block.timestamp,
                lockEnd: 0,
                permanent: true,
                isMaxTime: false
            });
            _votesCreated(msg.sender, permanentTokenId);
            _addTokenToUser(msg.sender, permanentTokenId);
            _mint(msg.sender, permanentTokenId);
            userPermanentLockId[msg.sender] = permanentTokenId;
            // Stake the freshly-minted permanent lock so it earns rewards (mirrors every other
            // lock-creation path); without this a converted permanent lock earns nothing until a
            // manual stake.
            _autoStakeIfEnabled(permanentTokenId);
            emit LockCreated(permanentTokenId, msg.sender, amount, 0, true);
        } else {
            // Add to existing permanent lock
            Lock memory prevPermanent = locks[permanentTokenId];
            locks[permanentTokenId].amount += amount;
            _moveVotes(msg.sender, prevPermanent, locks[permanentTokenId]);
            // Weight increased — checkpoint so the holder earns on the new balance, not a stale one.
            _autoCheckpointIfEnabled(permanentTokenId);
            emit LockIncreased(permanentTokenId, amount, 0);
        }

        // Burn the tokens (permanent lock = burn)
        ERC20Burnable(address(SLVR)).burn(amount);

        // Clean up normal lock (removing its remaining decaying votes)
        _votesRemoved(msg.sender, prevTimeLock);
        delete locks[tokenId];

        // Settle staking rewards to the owner and clean up weight before burning the old normal lock
        // (if staking set), so rewards accrued to it aren't stranded by the burn.
        _settleStakingBeforeBurn(tokenId, msg.sender);

        _burnAndRemove(msg.sender, tokenId);

        emit LockConvertedToPermanent(tokenId, permanentTokenId, amount);
    }

    /// @notice Withdraw after lock expires (only for normal locks)
    /// @param tokenId Token ID of the lock
    function withdraw(uint256 tokenId) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();

        Lock storage lock = locks[tokenId];
        if (lock.permanent) revert PermanentLock();
        if (block.timestamp < lock.lockEnd) revert LockNotExpired();

        uint256 amount = lock.amount;
        // Vote bookkeeping (a no-op for votes: the lock is expired, so its contribution is
        // already zero, and its scheduled decay stop fired in the past).
        _votesRemoved(msg.sender, lock);
        delete locks[tokenId];

        // Settle staking rewards to the owner and clean up weight before burning (if staking set).
        // Without this, rewards accrued to this token are stranded once the NFT is burned.
        _settleStakingBeforeBurn(tokenId, msg.sender);

        _burnAndRemove(msg.sender, tokenId);

        SLVR.safeTransfer(msg.sender, amount);

        emit LockWithdrawn(tokenId, msg.sender, amount);
    }

    /// @notice Get current voting power for a token (decays for normal locks)
    /// @param tokenId Token ID
    /// @return ve Current voting power
    function getVotingPower(uint256 tokenId) public view returns (uint256 ve) {
        Lock memory lock = locks[tokenId];
        if (lock.amount == 0) return 0;
        
        // If lock has expired (and not permanent), voting power is 0
        if (!lock.permanent && block.timestamp >= lock.lockEnd) {
            return 0;
        }
        
        if (lock.permanent) {
            // Permanent locks: voting power = amount * multiplier (constant, no decay)
            uint256 multiplier = getRewardMultiplier(tokenId);
            ve = (lock.amount * multiplier) / 1e18;
        } else {
            // Normal locks: voting power decays linearly based on time remaining
            // Calculate initial multiplier based on original lock duration
            uint256 originalDuration = lock.lockEnd - lock.lockStart;
            // Cap duration at TMAX to prevent multiplier from exceeding MMAX
            if (originalDuration > TMAX) {
                originalDuration = TMAX;
            }
            uint256 timeRemaining = lock.lockEnd > block.timestamp ? lock.lockEnd - block.timestamp : 0;
            
            // Initial multiplier at lock creation (based on original duration, capped at MMAX)
            uint256 initialMultiplier = 1e18 + ((MMAX - 1e18) * originalDuration) / TMAX;
            // Additional safety cap to ensure multiplier never exceeds MMAX
            if (initialMultiplier > MMAX) {
                initialMultiplier = MMAX;
            }
            
            // Voting power decays linearly: ve = amount * initial_multiplier * (time_remaining / original_duration)
            if (originalDuration == 0) return 0;
            ve = (lock.amount * initialMultiplier * timeRemaining) / (1e18 * originalDuration);
        }
    }

    /// @notice Get staking weight for a token (non-decaying, for reward distribution)
    /// @param tokenId Token ID
    /// @return w Staking weight (constant based on lock duration, does not decay over time)
    function getStakingWeight(uint256 tokenId) external view returns (uint256 w) {
        Lock memory lock = locks[tokenId];
        if (lock.amount == 0) return 0;

        // If lock has expired (and not permanent), return 0 weight
        if (!lock.permanent && block.timestamp >= lock.lockEnd) {
            return 0;
        }

        if (lock.permanent) {
            // Permanent locks: 4x multiplier (constant)
            uint256 m = 1e18 + ((MMAX - 1e18) * P) / 1e18;
            return (lock.amount * m) / 1e18;
        }

        // Normal locks: constant weight based on original duration at last reset (no decay)
        uint256 d = lock.lockEnd > lock.lockStart ? (lock.lockEnd - lock.lockStart) : 0;
        if (d > TMAX) d = TMAX;

        uint256 m0 = 1e18 + ((MMAX - 1e18) * d) / TMAX;
        if (m0 > MMAX) m0 = MMAX;

        return (lock.amount * m0) / 1e18;
    }

    /// @notice Get current reward multiplier for a token (decays for normal locks)
    /// @param tokenId Token ID
    /// @return m Current reward multiplier (in 1e18 format, e.g., 1.5e18 = 1.5x)
    function getRewardMultiplier(uint256 tokenId) public view returns (uint256 m) {
        Lock memory lock = locks[tokenId];
        if (lock.amount == 0) return 1e18; // 1x for no lock

        if (lock.permanent) {
            // Permanent lock: m = 1e18 + (MMAX - 1e18) * P / 1e18 = 4.0x
            m = 1e18 + ((MMAX - 1e18) * P) / 1e18;
        } else {
            // Normal lock: m = 1e18 + (MMAX - 1e18) * t / TMAX (decays linearly)
            uint256 t = lock.lockEnd > block.timestamp
                ? (lock.lockEnd - block.timestamp > TMAX ? TMAX : lock.lockEnd - block.timestamp)
                : 0;
            m = 1e18 + ((MMAX - 1e18) * t) / TMAX;
            // Safety cap to ensure multiplier never exceeds MMAX for normal locks
            if (m > MMAX) {
                m = MMAX;
            }
        }
    }

    /// @notice Get total voting power for a user across all their NFTs
    /// @param user User address
    /// @return totalVe Total voting power
    function getTotalVotingPower(address user) external view returns (uint256 totalVe) {
        uint256[] memory tokens = userTokens[user];
        for (uint256 i = 0; i < tokens.length; i++) {
            totalVe += getVotingPower(tokens[i]);
        }
    }

    /// @notice Get user's token IDs
    /// @param user User address
    /// @return Array of token IDs
    function getUserTokens(address user) external view returns (uint256[] memory) {
        return userTokens[user];
    }

    /// @notice Get lock details for a token
    /// @param tokenId Token ID
    /// @return lock Lock struct
    function getLock(uint256 tokenId) external view returns (Lock memory) {
        return locks[tokenId];
    }

    /// @notice Get the token ID of a user's permanent lock, if one exists
    /// @param user User address
    /// @return tokenId Token ID of permanent lock, or 0 if none exists
    function getPermanentLockTokenId(address user) public view returns (uint256 tokenId) {
        return userPermanentLockId[user];
    }

    /// @notice Get the token ID of a user's permanent lock, if one exists
    /// @param user User address
    /// @return tokenId Token ID of permanent lock, or 0 if none exists
    /// @dev Users can have multiple TMAX locks, so this only returns permanent lock
    function getMaxLockTokenId(address user) public view returns (uint256 tokenId) {
        return userPermanentLockId[user];
    }

    /// @notice Override tokenURI to use external metadata contract
    /// @param tokenId The token ID
    /// @return The token URI (JSON metadata)
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        _requireOwned(tokenId);
        
        if (address(metadataContract) == address(0)) {
            // Return empty string if no metadata contract is set
            return "";
        }
        
        return metadataContract.tokenURI(tokenId, address(this));
    }

    /// @notice Set or update the metadata contract address (owner only)
    /// @param _metadataContract The address of the new metadata contract
    function setMetadataContract(address _metadataContract) external onlyOwner {
        address oldContract = address(metadataContract);
        metadataContract = ISlvrVoteEscrowMetadata(_metadataContract);
        emit MetadataContractUpdated(oldContract, _metadataContract);
    }

    /// @notice Set or update the staking contract address (owner only)
    /// @param _stakingContract The address of the staking contract (can be address(0) to disable)
    function setStakingContract(address _stakingContract) external onlyOwner {
        address oldContract = address(stakingContract);
        stakingContract = ISlvrVoteEscrowStaking(_stakingContract);
        emit StakingContractUpdated(oldContract, _stakingContract);
    }

    // ------------------------------------------------------------------
    // Governance votes (ERC-5805 / ERC-6372, timestamp clock)
    // ------------------------------------------------------------------

    /// @notice ERC-6372 clock: timestamps, matching the lock/decay math.
    function clock() public view returns (uint48) {
        return uint48(block.timestamp);
    }

    /// @notice ERC-6372 clock mode descriptor.
    // solhint-disable-next-line func-name-mixedcase
    function CLOCK_MODE() public pure returns (string memory) {
        return "mode=timestamp";
    }

    /// @notice Current governance voting power of `account` (checkpointed; see contract @dev for
    ///         how it relates to getTotalVotingPower).
    function getVotes(address account) public view returns (uint256) {
        return _accountVotes[account].valueAt(block.timestamp);
    }

    /// @notice Governance voting power of `account` at a past `timepoint` (seconds).
    function getPastVotes(address account, uint256 timepoint) public view returns (uint256) {
        uint48 current = clock();
        if (timepoint >= current) revert ERC5805FutureLookup(timepoint, current);
        return _accountVotes[account].valueAt(timepoint);
    }

    /// @notice Total governance voting power at a past `timepoint` (for Governor quorum math).
    /// @dev Includes protocol-held permanent locks (team vesting / growth fund); account for
    ///      that when picking a quorum fraction.
    function getPastTotalSupply(uint256 timepoint) public view returns (uint256) {
        uint48 current = clock();
        if (timepoint >= current) revert ERC5805FutureLookup(timepoint, current);
        return _totalVotes.valueAt(timepoint);
    }

    /// @notice Votes are soulbound to the holder; every account is its own delegate.
    function delegates(address account) public pure returns (address) {
        return account;
    }

    /// @notice Delegation is not supported (soulbound votes). Self-delegation is a no-op so
    ///         tooling that calls delegate(self) to "activate" votes keeps working.
    function delegate(address delegatee) public view {
        if (delegatee != msg.sender) revert DelegationNotSupported();
    }

    /// @notice Delegation by signature is not supported (soulbound votes).
    function delegateBySig(address, uint256, uint256, uint8, bytes32, bytes32) public pure {
        revert DelegationNotSupported();
    }

    /// @dev Governance contribution of a lock at the current time, as (bias, slope, govEnd):
    ///      bias is the power now, slope its per-second decay, govEnd the day-aligned time the
    ///      decay is scheduled to stop. Permanent locks are pure bias (no decay). Mirrors
    ///      getVotingPower exactly, except power is measured to govEnd (lockEnd rounded down to
    ///      a day) so decay stops are schedulable at bucket boundaries.
    function _voteParams(Lock memory lock) private view returns (int256 bias, int256 slope, uint256 govEnd) {
        if (lock.amount == 0) return (0, 0, 0);

        if (lock.permanent) {
            uint256 m = 1e18 + ((MMAX - 1e18) * P) / 1e18;
            return (int256((lock.amount * m) / 1e18), 0, 0);
        }

        if (lock.lockEnd <= block.timestamp) return (0, 0, 0);
        govEnd = (lock.lockEnd / VoteCheckpointLib.BUCKET) * VoteCheckpointLib.BUCKET;
        if (govEnd <= block.timestamp) return (0, 0, 0);

        uint256 duration = lock.lockEnd - lock.lockStart;
        if (duration > TMAX) duration = TMAX;
        if (duration == 0) return (0, 0, 0);
        uint256 m0 = 1e18 + ((MMAX - 1e18) * duration) / TMAX;
        if (m0 > MMAX) m0 = MMAX;

        uint256 s = (lock.amount * m0) / (1e18 * duration); // ve per second
        return (int256(s * (govEnd - block.timestamp)), int256(s), govEnd);
    }

    /// @dev Move an account's checkpointed votes from a lock's old state to its new state.
    ///      Pass a zero-amount Lock for "no lock" (creation/removal).
    function _moveVotes(address account, Lock memory oldLock, Lock memory newLock) private {
        (int256 ob, int256 os, uint256 oe) = _voteParams(oldLock);
        (int256 nb, int256 ns, uint256 ne) = _voteParams(newLock);
        if (ob == nb && os == ns && oe == ne) return; // e.g. bookkeeping on an expired lock
        _accountVotes[account].checkpoint(ob, os, oe, nb, ns, ne);
        _totalVotes.checkpoint(ob, os, oe, nb, ns, ne);
    }

    /// @dev Checkpoint votes for a freshly created lock.
    function _votesCreated(address account, uint256 tokenId) private {
        Lock memory none;
        _moveVotes(account, none, locks[tokenId]);
    }

    /// @dev Checkpoint votes for a removed (withdrawn/converted) lock.
    function _votesRemoved(address account, Lock memory prev) private {
        Lock memory none;
        _moveVotes(account, prev, none);
    }

    /// @notice Override approve to revert (soulbound NFT - non-transferable)
    /// @dev This NFT is soulbound, so approvals are not allowed
    function approve(address, uint256) public pure override {
        revert SoulboundNFT();
    }

    /// @notice Override setApprovalForAll to revert (soulbound NFT - non-transferable)
    /// @dev This NFT is soulbound, so approvals are not allowed
    function setApprovalForAll(address, bool) public pure override {
        revert SoulboundNFT();
    }

    /// @notice Override transfer to prevent transfers (non-transferable NFTs)
    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        address from = _ownerOf(tokenId);
        // Allow minting (from == address(0)) and burning (to == address(0))
        // But prevent transfers between users (to != from when from != address(0))
        if (from != address(0) && to != address(0) && to != from) revert NonTransferable();
        
        // Clean up permanent lock tracking if token is being burned
        if (to == address(0) && from != address(0)) {
            if (userPermanentLockId[from] == tokenId) {
                delete userPermanentLockId[from];
            }
        }
        
        return super._update(to, tokenId, auth);
    }


    /// @notice Add token to user's token list with O(1) index tracking
    function _addTokenToUser(address user, uint256 tokenId) private {
        uint256[] storage tokens = userTokens[user];
        uint256 index = tokens.length;
        tokens.push(tokenId);
        // Store index+1 (1-based indexing, 0 means not in list)
        tokenIndexInUserList[tokenId] = index + 1;
    }

    /// @notice Remove token from user's token list using O(1) swap-with-last
    function _removeTokenFromUser(address user, uint256 tokenId) private {
        uint256[] storage tokens = userTokens[user];
        uint256 indexPlusOne = tokenIndexInUserList[tokenId];
        if (indexPlusOne == 0) revert TokenNotInList();
        
        uint256 index = indexPlusOne - 1; // Convert to 0-based index
        uint256 lastIndex = tokens.length - 1;
        
        if (index != lastIndex) {
            // Swap with last element
            uint256 lastTokenId = tokens[lastIndex];
            tokens[index] = lastTokenId;
            // Update the moved token's index
            tokenIndexInUserList[lastTokenId] = index + 1;
        }
        
        // Remove last element
        tokens.pop();
        // Clear the removed token's index (0 means not in list)
        delete tokenIndexInUserList[tokenId];
    }

    /// @notice Internal helper to settle staking rewards and clean up weight before burning (if set)
    /// @dev Calls claimOnBurn, which accrues the token's pending staker rewards, removes its weight,
    ///      and re-keys the rewards to `owner` (pull-based) so they survive the burn. Best-effort
    ///      (try/catch) so a broken staking contract can never block withdrawal of the SLVR principal.
    /// @param tokenId The token ID being burned
    /// @param owner The soon-to-be-former owner to credit any accrued rewards to
    function _settleStakingBeforeBurn(uint256 tokenId, address owner) private {
        if (address(stakingContract) != address(0)) {
            try stakingContract.claimOnBurn(tokenId, owner) {
                // Success - rewards settled to owner, weight cleaned up
            } catch {
                // Best-effort: never block the burn / principal withdrawal on a staking issue.
            }
        }
    }

    /// @notice Internal helper to burn a token and remove it from user's token list
    /// @dev Ensures _removeTokenFromUser is always called when burning to maintain userTokens consistency
    /// @param user The owner of the token
    /// @param tokenId The token ID to burn
    function _burnAndRemove(address user, uint256 tokenId) internal {
        _burn(tokenId);
        _removeTokenFromUser(user, tokenId);
    }

    /// @notice Internal helper to optionally stake a token in the staking contract (if enabled)
    /// @dev Called after minting a new token to automatically stake it
    /// @param tokenId The token ID to stake
    function _autoStakeIfEnabled(uint256 tokenId) private {
        if (address(stakingContract) != address(0)) {
            try stakingContract.stake(tokenId) {
                // Success - token staked
            } catch {
                // Ignore errors - stake() may fail if token already staked or contract issue
                // This is best-effort auto-staking, shouldn't block the mint
            }
        }
    }

    /// @notice Internal helper to optionally checkpoint a token in the staking contract (if enabled)
    /// @dev Called after updating a lock to checkpoint weight changes
    /// @param tokenId The token ID to checkpoint
    function _autoCheckpointIfEnabled(uint256 tokenId) private {
        if (address(stakingContract) != address(0)) {
            try stakingContract.checkpoint(tokenId) {
                // Success - token checkpointed
            } catch {
                // Ignore errors - checkpoint() may fail if token not staked or contract issue
                // This is best-effort auto-checkpointing, shouldn't block the update
            }
        }
    }
}
Chain explorer2089msChain node94ms