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
- Creator
- 0xf2a032144f…FE8510461d
- Creation tx
- 0x1c63234b74…f7743713d5
Token
- Name
- veOIL
- Symbol
- veOIL
- Decimals
- —
- Holders
- 41
- Market
- View token page →
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