OilVoteEscrow

0x94d07fb3674f5c33a0fcd0d65455e34581c3ddfc

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
20,463 bytes
ABI entries
86
23 read · 21 write
License
none

Contract information

Address
0x94d07fb3674f5c33a0fcd0d65455e34581c3ddfc
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x1c63234b74…f7743713d5

Token

Name
veOIL
Symbol
veOIL
Decimals
Holders
41

Read contract (23)

approve(address, uint256)
authorizedContract()address
balanceOf(address)uint256
getApproved(uint256)address
getLock(uint256)tuple
getStakingWeight(uint256)uint256
getUserTokens(address)uint256[]
isApprovedForAll(address, address)bool
locks(uint256)uint256, uint256, uint256, bool
name()string
oil()address
owner()address
ownerOf(uint256)address
pendingOwner()address
rewardsLedger()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 (13)

ApprovalApprovalForAllAuthorizedContractUpdatedLockConvertedToPermanentLockCreatedLockExtendedLockIncreasedLockWithdrawnOwnershipTransferStartedOwnershipTransferredRewardsLedgerUpdatedStakingContractUpdatedTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "oil_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "AlreadyPermanent",
    "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": "LockExpired",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockNotExpired",
    "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": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SoulboundNFT",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TokenNotInList",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "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": "authorized",
        "type": "address"
      }
    ],
    "name": "AuthorizedContractUpdated",
    "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": "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": "rewardsLedger",
        "type": "address"
      }
    ],
    "name": "RewardsLedgerUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "staking",
        "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": "acceptOwnership",
    "outputs": [],
    "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": [
      {
        "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": "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": "struct OilVoteEscrow.Lock",
        "name": "",
        "type": "tuple"
      }
    ],
    "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": "getUserTokens",
    "outputs": [
      {
        "internalType": "uint256[]",
        "name": "",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "increaseAmount",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "increaseAmountFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "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"
      }
    ],
    "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"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "oil",
    "outputs": [
      {
        "internalType": "contract IOil",
        "name": "",
        "type": "address"
      }
    ],
    "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": [],
    "name": "rewardsLedger",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "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": "authorized_",
        "type": "address"
      }
    ],
    "name": "setAuthorizedContract",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "rewardsLedger_",
        "type": "address"
      }
    ],
    "name": "setRewardsLedger",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "staking_",
        "type": "address"
      }
    ],
    "name": "setStakingContract",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "stakePermanentFor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stakingContract",
    "outputs": [
      {
        "internalType": "contract IOilVeStaking",
        "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.28;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.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 {IOil} from "./interfaces/IOil.sol";
import {IOilVeStaking} from "./interfaces/IOilVoteEscrow.sol";
import {Constants} from "./libraries/Constants.sol";
import {VeOilMetadata} from "./libraries/VeOilMetadata.sol";

/// @title OilVoteEscrow
/// @notice Soulbound veOIL locks: timed (1×–3× / ≤120d) or permanent (5×, burned).
contract OilVoteEscrow is ERC721, Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;

    error ZeroAddress();
    error ZeroAmount();
    error InvalidDuration();
    error LockEndExceedsMax();
    error NotOwner();
    error PermanentLockExists();
    error PermanentLock();
    error NotPermanentLock();
    error AlreadyPermanent();
    error LockNotExpired();
    error LockExpired();
    error LockEmpty();
    error SoulboundNFT();
    error NonTransferable();
    error NotAuthorized();
    error TokenNotInList();

    IOil public immutable oil;
    IOilVeStaking public stakingContract;
    address public authorizedContract;
    /// @dev RewardsLedger — stake-from-unrefined (createLockFor / stakePermanentFor).
    address public rewardsLedger;

    uint256 private _tokenIdCounter = 1;

    struct Lock {
        uint256 amount;
        uint256 lockStart;
        uint256 lockEnd;
        bool permanent;
    }

    mapping(uint256 => Lock) public locks;
    mapping(address => uint256[]) public userTokens;
    mapping(uint256 => uint256) public tokenIndexInUserList; // tokenId => index+1
    mapping(address => uint256) public userPermanentLockId;

    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 StakingContractUpdated(address indexed staking);
    event AuthorizedContractUpdated(address indexed authorized);
    event RewardsLedgerUpdated(address indexed rewardsLedger);

    constructor(address oil_, address owner_) ERC721("veOIL", "veOIL") Ownable(owner_) {
        if (oil_ == address(0) || owner_ == address(0)) revert ZeroAddress();
        oil = IOil(oil_);
    }

    function setStakingContract(address staking_) external onlyOwner {
        stakingContract = IOilVeStaking(staking_);
        emit StakingContractUpdated(staking_);
    }

    function setAuthorizedContract(address authorized_) external onlyOwner {
        authorizedContract = authorized_;
        emit AuthorizedContractUpdated(authorized_);
    }

    function setRewardsLedger(address rewardsLedger_) external onlyOwner {
        if (rewardsLedger_ == address(0)) revert ZeroAddress();
        rewardsLedger = rewardsLedger_;
        emit RewardsLedgerUpdated(rewardsLedger_);
    }

    function createLock(uint256 amount, uint256 duration) external nonReentrant returns (uint256 tokenId) {
        if (amount == 0) revert ZeroAmount();
        if (duration < Constants.MIN_LOCK_DURATION || duration > Constants.MAX_LOCK_DURATION) {
            revert InvalidDuration();
        }

        uint256 received = _pull(msg.sender, amount);
        tokenId = _tokenIdCounter++;
        uint256 lockEnd = block.timestamp + duration;

        locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: lockEnd, permanent: false});
        _addTokenToUser(msg.sender, tokenId);
        _mint(msg.sender, tokenId);
        _autoStake(tokenId);

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

    function createPermanentLock(uint256 amount) external nonReentrant returns (uint256 tokenId) {
        tokenId = _createPermanent(msg.sender, msg.sender, amount);
    }

    /// @notice Authorized protocol path (e.g. TeamVesting): pull from caller, mint permanent to `user`.
    function createPermanentLockFor(address user, uint256 amount) external nonReentrant returns (uint256 tokenId) {
        if (msg.sender != authorizedContract) revert NotAuthorized();
        if (user == address(0)) revert ZeroAddress();
        tokenId = _createPermanent(msg.sender, user, amount);
    }

    /// @notice RewardsLedger: create a timed lock for `user`, pulling OIL from the ledger.
    function createLockFor(address user, uint256 amount, uint256 duration)
        external
        nonReentrant
        returns (uint256 tokenId)
    {
        if (msg.sender != rewardsLedger) revert NotAuthorized();
        if (user == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (duration < Constants.MIN_LOCK_DURATION || duration > Constants.MAX_LOCK_DURATION) {
            revert InvalidDuration();
        }

        uint256 received = _pull(msg.sender, amount);
        tokenId = _tokenIdCounter++;
        uint256 lockEnd = block.timestamp + duration;

        locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: lockEnd, permanent: false});
        _addTokenToUser(user, tokenId);
        _mint(user, tokenId);
        _autoStake(tokenId);

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

    /// @notice RewardsLedger: create or increase `user`'s permanent lock, pulling OIL from the ledger.
    function stakePermanentFor(address user, uint256 amount) external nonReentrant returns (uint256 tokenId) {
        if (msg.sender != rewardsLedger) revert NotAuthorized();
        if (user == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();

        tokenId = userPermanentLockId[user];
        if (tokenId == 0) {
            return _createPermanent(msg.sender, user, amount);
        }

        Lock storage lock = locks[tokenId];
        uint256 received = _pull(msg.sender, amount);
        oil.burn(received);
        lock.amount += received;
        _autoCheckpoint(tokenId);
        emit LockIncreased(tokenId, received, 0);
    }

    /// @notice RewardsLedger: add OIL to an existing lock owned by `user` (timed or permanent).
    function increaseAmountFor(address user, uint256 tokenId, uint256 amount)
        external
        nonReentrant
        returns (uint256)
    {
        if (msg.sender != rewardsLedger) revert NotAuthorized();
        if (user == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        if (ownerOf(tokenId) != user) revert NotOwner();

        Lock storage lock = locks[tokenId];
        if (lock.permanent) {
            uint256 receivedPerm = _pull(msg.sender, amount);
            oil.burn(receivedPerm);
            lock.amount += receivedPerm;
            _autoCheckpoint(tokenId);
            emit LockIncreased(tokenId, receivedPerm, 0);
            return tokenId;
        }

        if (lock.amount == 0) revert LockEmpty();
        if (block.timestamp >= lock.lockEnd) revert LockExpired();

        uint256 received = _pull(msg.sender, amount);
        lock.amount += received;
        _autoCheckpoint(tokenId);
        emit LockIncreased(tokenId, received, lock.lockEnd);
        return tokenId;
    }

    /// @notice Add OIL to an existing lock (same unlock for timed; burn for permanent).
    function increaseAmount(uint256 tokenId, uint256 amount) external nonReentrant {
        _increaseAmount(tokenId, amount, 0, false);
    }

    /// @notice Add OIL to a timed lock and optionally extend duration.
    /// @param newDuration 0 = keep unlock; MAX_LOCK_DURATION = reset to max from now;
    ///                    otherwise add that many seconds to current end (capped at now + max).
    function increaseLock(uint256 tokenId, uint256 amount, uint256 newDuration) external nonReentrant {
        _increaseAmount(tokenId, amount, newDuration, false);
    }

    /// @notice Add OIL to a permanent lock (burns).
    function increasePermanentLock(uint256 tokenId, uint256 amount) external nonReentrant {
        _increaseAmount(tokenId, amount, 0, true);
    }

    function _increaseAmount(uint256 tokenId, uint256 amount, uint256 newDuration, bool permanentOnly) internal {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();
        if (amount == 0) revert ZeroAmount();
        Lock storage lock = locks[tokenId];

        if (lock.permanent) {
            if (newDuration != 0) revert PermanentLock();
            uint256 received = _pull(msg.sender, amount);
            oil.burn(received);
            lock.amount += received;
            _autoCheckpoint(tokenId);
            emit LockIncreased(tokenId, received, 0);
            return;
        }

        if (permanentOnly) revert NotPermanentLock();
        if (lock.amount == 0) revert LockEmpty();
        if (block.timestamp >= lock.lockEnd) revert LockExpired();

        uint256 receivedTimed = _pull(msg.sender, amount);
        lock.amount += receivedTimed;
        if (newDuration > 0) {
            _applyExtend(lock, newDuration);
        }
        _autoCheckpoint(tokenId);
        emit LockIncreased(tokenId, receivedTimed, lock.lockEnd);
    }

    /// @notice Extend a timed lock (resets start; weight snaps to the new remaining duration).
    function extendLock(uint256 tokenId, uint256 duration) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();
        if (duration == 0 || duration > Constants.MAX_LOCK_DURATION) revert InvalidDuration();

        Lock storage lock = locks[tokenId];
        if (lock.permanent) revert PermanentLock();
        if (lock.amount == 0) revert LockEmpty();

        _applyExtend(lock, duration);
        _autoCheckpoint(tokenId);
        emit LockExtended(tokenId, lock.lockEnd);
    }

    /// @notice Convert a timed lock into the wallet's permanent lock (burns OIL + old NFT).
    function convertToPermanentLock(uint256 tokenId) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();

        Lock memory timed = locks[tokenId];
        if (timed.permanent) revert AlreadyPermanent();
        uint256 amount = timed.amount;
        if (amount == 0) revert LockEmpty();

        uint256 permanentTokenId = userPermanentLockId[msg.sender];
        if (permanentTokenId == 0) {
            permanentTokenId = _tokenIdCounter++;
            locks[permanentTokenId] =
                Lock({amount: amount, lockStart: block.timestamp, lockEnd: 0, permanent: true});
            userPermanentLockId[msg.sender] = permanentTokenId;
            _addTokenToUser(msg.sender, permanentTokenId);
            _mint(msg.sender, permanentTokenId);
            _autoStake(permanentTokenId);
            emit LockCreated(permanentTokenId, msg.sender, amount, 0, true);
        } else {
            locks[permanentTokenId].amount += amount;
            _autoCheckpoint(permanentTokenId);
            emit LockIncreased(permanentTokenId, amount, 0);
        }

        oil.burn(amount);
        delete locks[tokenId];
        _settleBeforeBurn(tokenId, msg.sender);
        _burnAndRemove(msg.sender, tokenId);

        emit LockConvertedToPermanent(tokenId, permanentTokenId, amount);
    }

    function _applyExtend(Lock storage lock, uint256 duration) internal {
        if (duration == 0 || duration > Constants.MAX_LOCK_DURATION) revert InvalidDuration();

        uint256 proposedEnd;
        if (duration == Constants.MAX_LOCK_DURATION) {
            proposedEnd = block.timestamp + Constants.MAX_LOCK_DURATION;
        } else {
            uint256 currentEnd = lock.lockEnd > block.timestamp ? lock.lockEnd : block.timestamp;
            proposedEnd = currentEnd + duration;
        }
        if (proposedEnd > block.timestamp + Constants.MAX_LOCK_DURATION) revert LockEndExceedsMax();

        lock.lockEnd = proposedEnd;
        lock.lockStart = block.timestamp;
    }

    function withdraw(uint256 tokenId) external nonReentrant {
        if (ownerOf(tokenId) != msg.sender) revert NotOwner();
        Lock memory lock = locks[tokenId];
        if (lock.permanent) revert PermanentLock();
        if (block.timestamp < lock.lockEnd) revert LockNotExpired();
        if (lock.amount == 0) revert LockEmpty();

        uint256 amount = lock.amount;
        delete locks[tokenId];
        _settleBeforeBurn(tokenId, msg.sender);
        _burnAndRemove(msg.sender, tokenId);
        IERC20(address(oil)).safeTransfer(msg.sender, amount);
        emit LockWithdrawn(tokenId, msg.sender, amount);
    }

    function getStakingWeight(uint256 tokenId) public view returns (uint256 w) {
        Lock memory lock = locks[tokenId];
        if (lock.amount == 0) return 0;
        if (!lock.permanent && block.timestamp >= lock.lockEnd) return 0;

        uint256 mult;
        if (lock.permanent) {
            mult = Constants.PERMANENT_STAKE_MULTIPLIER * Constants.STAKE_MULTIPLIER_SCALE;
        } else {
            uint256 d = lock.lockEnd > lock.lockStart ? lock.lockEnd - lock.lockStart : 0;
            if (d > Constants.MAX_LOCK_DURATION) d = Constants.MAX_LOCK_DURATION;
            mult = Constants.STAKE_MULTIPLIER_SCALE
                + ((Constants.MAX_STAKE_MULTIPLIER - 1) * d * Constants.STAKE_MULTIPLIER_SCALE)
                    / Constants.MAX_LOCK_DURATION;
        }
        return (lock.amount * mult) / Constants.STAKE_MULTIPLIER_SCALE;
    }

    function getLock(uint256 tokenId) external view returns (Lock memory) {
        return locks[tokenId];
    }

    function getUserTokens(address user) external view returns (uint256[] memory) {
        return userTokens[user];
    }

    /// @notice Dynamic ERC-721 metadata (JSON + on-chain SVG) from live lock state.
    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        _requireOwned(tokenId);
        Lock memory lock = locks[tokenId];
        return VeOilMetadata.tokenURI(
            VeOilMetadata.Card({
                tokenId: tokenId,
                amount: lock.amount,
                votingPower: getStakingWeight(tokenId),
                multiplierScaled: _multiplierScaled(lock),
                permanent: lock.permanent,
                lockStart: lock.lockStart,
                lockEnd: lock.lockEnd,
                nowTs: block.timestamp
            })
        );
    }

    function _multiplierScaled(Lock memory lock) internal pure returns (uint256) {
        if (lock.permanent) {
            return Constants.PERMANENT_STAKE_MULTIPLIER * Constants.STAKE_MULTIPLIER_SCALE;
        }
        uint256 d = lock.lockEnd > lock.lockStart ? lock.lockEnd - lock.lockStart : 0;
        if (d > Constants.MAX_LOCK_DURATION) d = Constants.MAX_LOCK_DURATION;
        return Constants.STAKE_MULTIPLIER_SCALE
            + ((Constants.MAX_STAKE_MULTIPLIER - 1) * d * Constants.STAKE_MULTIPLIER_SCALE) / Constants.MAX_LOCK_DURATION;
    }

    function _createPermanent(address from, address user, uint256 amount) internal returns (uint256 tokenId) {
        if (amount == 0) revert ZeroAmount();
        if (userPermanentLockId[user] != 0) revert PermanentLockExists();

        uint256 received = _pull(from, amount);
        oil.burn(received);

        tokenId = _tokenIdCounter++;
        locks[tokenId] = Lock({amount: received, lockStart: block.timestamp, lockEnd: 0, permanent: true});
        userPermanentLockId[user] = tokenId;
        _addTokenToUser(user, tokenId);
        _mint(user, tokenId);
        _autoStake(tokenId);

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

    function _pull(address from, uint256 amount) internal returns (uint256 received) {
        uint256 beforeBal = IERC20(address(oil)).balanceOf(address(this));
        IERC20(address(oil)).safeTransferFrom(from, address(this), amount);
        received = IERC20(address(oil)).balanceOf(address(this)) - beforeBal;
        if (received == 0) revert ZeroAmount();
    }

    function _autoStake(uint256 tokenId) internal {
        if (address(stakingContract) == address(0)) return;
        try stakingContract.stake(tokenId) {} catch {}
    }

    function _autoCheckpoint(uint256 tokenId) internal {
        if (address(stakingContract) == address(0)) return;
        try stakingContract.checkpoint(tokenId) {} catch {}
    }

    function _settleBeforeBurn(uint256 tokenId, address owner_) internal {
        if (address(stakingContract) == address(0)) return;
        try stakingContract.claimOnBurn(tokenId, owner_) {} catch {}
    }

    function _addTokenToUser(address user, uint256 tokenId) internal {
        uint256[] storage tokens = userTokens[user];
        tokens.push(tokenId);
        tokenIndexInUserList[tokenId] = tokens.length;
    }

    function _removeTokenFromUser(address user, uint256 tokenId) internal {
        uint256[] storage tokens = userTokens[user];
        uint256 indexPlusOne = tokenIndexInUserList[tokenId];
        if (indexPlusOne == 0) revert TokenNotInList();
        uint256 index = indexPlusOne - 1;
        uint256 last = tokens.length - 1;
        if (index != last) {
            uint256 lastId = tokens[last];
            tokens[index] = lastId;
            tokenIndexInUserList[lastId] = index + 1;
        }
        tokens.pop();
        delete tokenIndexInUserList[tokenId];
    }

    function _burnAndRemove(address user, uint256 tokenId) internal {
        if (userPermanentLockId[user] == tokenId) delete userPermanentLockId[user];
        _burn(tokenId);
        _removeTokenFromUser(user, tokenId);
    }

    function approve(address, uint256) public pure override {
        revert SoulboundNFT();
    }

    function setApprovalForAll(address, bool) public pure override {
        revert SoulboundNFT();
    }

    function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
        address from = _ownerOf(tokenId);
        if (from != address(0) && to != address(0) && to != from) revert NonTransferable();
        if (to == address(0) && from != address(0) && userPermanentLockId[from] == tokenId) {
            delete userPermanentLockId[from];
        }
        return super._update(to, tokenId, auth);
    }
}
Chain explorer8043msChain node83ms