03 / Contract reference

VellumVault
reference.

The production vault is a single 200-line Solidity file with no imports, no proxy and no external dependencies. This page documents its entire public surface.

At a glance

#
PropertyValue
Sourcecontracts/VellumVault.sol
Compilersolidity ^0.8.30, optimizer on, 200 runs
LicenseMIT
Token name / symbolVellum Note / VNOTE
InterfacesERC-165 (0x01ffc9a7), ERC-721 core (0x80ac58cd)
UpgradeabilityNone — no proxy, no delegatecall, no selfdestruct
Privileged roleguardian (pause new wraps, rotate itself)
ReentrancyMutex on wrap and claim
ABI / bytecodeapp/lib/vellumVaultArtifact.ts, generated on npm run build

The constructor takes one argument, initialGuardian, which must be non-zero. Deployment is covered in the developer guide.

Constants

#
NameTypeValueMeaning
namestring"Vellum Note"ERC-721 collection name
symbolstring"VNOTE"ERC-721 collection symbol
MIN_TERMuint641 daysSmallest non-zero term (86 400 s)
MAX_TERMuint643650 daysLargest term (315 360 000 s)

Storage & views

#
GetterReturnsNotes
positions(uint256 id)(address token, uint256 amount, uint64 maturity, bool claimed)Zeroed for ids that were never minted. Remains readable after claim.
ownerOf(uint256 id)addressReverts Vellum: unknown note for unminted or burned ids.
isClaimable(uint256 id)booltrue iff the note exists and its position is unclaimed. Never reverts.
balanceOf(address)uint256Number of live notes held by the address.
getApproved(uint256 id)addressPer-token approval. Cleared on transfer and claim. Does not revert for unknown ids.
isApprovedForAll(owner, op)boolOperator approval.
nextTokenId()uint256Id the next wrap will mint. Starts at 1.
guardian()addressCurrent guardian.
pendingGuardian()addressProposed guardian awaiting acceptance, or zero.
wrapsPaused()boolWhen true, wrap reverts. Claims and transfers are unaffected.
supportsInterface(bytes4)boolERC-165 and ERC-721 only. ERC-721 Metadata / Enumerable are not implemented.

Core functions

#
function wrap(address token, uint256 amount, uint64 termSeconds) external nonReentrant returns (uint256 tokenId)

Deposits exactly amount of token from msg.sender and mints a note to them.

  • Requires !wrapsPaused, token != 0, amount != 0.
  • Requires termSeconds == 0 || (MIN_TERM ≤ termSeconds ≤ MAX_TERM).
  • Pulls tokens with a low-level transferFrom call that accepts both boolean-returning and non-returning ERC-20s (USDT-style).
  • Requires the vault's balance to have grown by exactly amount.
  • Stores maturity = uint64(block.timestamp) + termSeconds.
  • Emits Transfer and NoteWrapped. Returns the new id.
function claim(uint256 tokenId) external nonReentrant

Burns the note and sends its full underlying balance to the caller.

  • Requires msg.sender == ownerOf(tokenId) — approvals do not grant claim rights.
  • Requires the position is unclaimed; marks it claimed before the transfer.
  • Does not check maturity.
  • Emits Transfer(owner, 0, id) and NoteClaimed.
function isClaimable(uint256 tokenId) external view returns (bool)

Convenience read for integrators: _ownerOf[id] != 0 && !positions[id].claimed.

ERC-721 surface

#
function transferFrom(address from, address to, uint256 tokenId) public

Requires to != 0, ownerOf(tokenId) == from, and that the caller is the owner, the approved address or an operator. Clears the per-token approval.

function safeTransferFrom(address from, address to, uint256 tokenId[, bytes data]) external / public

Performs transferFrom, then, if to has code, calls onERC721Received and requires the ERC-721 magic value. A recipient that does not implement the hook makes the call revert (with the recipient's own reason if any, otherwise an empty revert); a recipient that returns the wrong value reverts with Vellum: unsafe recipient.

function approve(address approved, uint256 tokenId) external

Caller must be the owner or an operator for the owner. Emits Approval.

function setApprovalForAll(address operator, bool approved) external

Rejects operator == msg.sender. Emits ApprovalForAll.

Not implemented on purpose

No tokenURI, no totalSupply, no enumeration, no burn other than claim, no mint other than wrap. Wallets that require ERC-721 Metadata will show the note by contract and id only.

Guardian functions

#
function setWrapsPaused(bool paused) external onlyGuardian

Emergency brake for new deposits. Has no effect on existing notes, transfers or claims. Emits WrapsPauseSet.

function proposeGuardian(address nextGuardian) external onlyGuardian

Records pendingGuardian. Requires a non-zero address. Emits GuardianTransferProposed. The current guardian keeps its role until the proposal is accepted.

function acceptGuardian() external

Callable only by pendingGuardian. Assigns the role, clears the pending slot and emits GuardianTransferred.

Events

#
solidityevent Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

event NoteWrapped(uint256 indexed tokenId, address indexed owner, address indexed token, uint256 amount, uint64 maturity);
event NoteClaimed(uint256 indexed tokenId, address indexed owner, address indexed token, uint256 amount);

event WrapsPauseSet(bool paused);
event GuardianTransferProposed(address indexed currentGuardian, address indexed pendingGuardian);
event GuardianTransferred(address indexed previousGuardian, address indexed newGuardian);

Revert reasons

#

Every require in the contract carries a Vellum:-prefixed string, so failures are readable in wallets and explorers.

ReasonRaised byWhen
Vellum: guardian requiredconstructor, proposeGuardianZero address given for guardian
Vellum: guardian onlysetWrapsPaused, proposeGuardianCaller is not the guardian
Vellum: pending guardian onlyacceptGuardianCaller is not the proposed guardian
Vellum: reentrant callwrap, claimRe-entered while a wrap or claim is in progress
Vellum: unknown noteownerOf and everything that uses itId never minted or already burned
Vellum: not authorisedapprove, transferFromCaller lacks owner/approval/operator rights
Vellum: self approvalsetApprovalForAllOperator equals caller
Vellum: zero recipienttransferFromto == address(0)
Vellum: wrong ownertransferFromfrom is not the current owner
Vellum: unsafe recipientsafeTransferFromRecipient contract returned the wrong selector
Vellum: wraps pausedwrapGuardian has paused deposits
Vellum: token requiredwraptoken == address(0)
Vellum: amount requiredwrapamount == 0
Vellum: invalid termwrapterm is non-zero and outside [1 day, 3650 days]
Vellum: deposit failedwrapERC-20 transferFrom reverted or returned false
Vellum: unsupported token transferwrapVault balance did not grow by exactly amount
Vellum: holder onlyclaimCaller is not the current owner
Vellum: already claimedclaimPosition already claimed (defensive; unreachable after burn)
Vellum: release failedclaimERC-20 transfer reverted or returned false

VellumTestVault

#

contracts/VellumTestVault.sol is a stripped-down variant for test tokens on Base Sepolia. It shares the wrap / transfer / claim shape but is not the production contract and should never hold real value. Differences:

AspectVellumVault (production)VellumTestVault
NameVellum NoteVellum Test Note
Amount typeuint256uint128
Minimum term1 day60 seconds
Position fieldstoken, amount, maturity, claimed+ creator (the wrapping address)
Exact-balance checkYes (rejects fee-on-transfer)No — trusts transferFrom's return value
Guardian / pauseYesNone
safeTransferFromYesNot implemented (transferFrom only)
Non-boolean ERC-20sSupportedRejected (requires a true return)

Both contracts are compiled by scripts/compile-test-vault.mjs into app/lib/vellumTestVaultArtifact.ts and app/lib/vellumVaultArtifact.ts. The app only ever calls the production ABI.