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
#| Property | Value |
|---|---|
| Source | contracts/VellumVault.sol |
| Compiler | solidity ^0.8.30, optimizer on, 200 runs |
| License | MIT |
| Token name / symbol | Vellum Note / VNOTE |
| Interfaces | ERC-165 (0x01ffc9a7), ERC-721 core (0x80ac58cd) |
| Upgradeability | None — no proxy, no delegatecall, no selfdestruct |
| Privileged role | guardian (pause new wraps, rotate itself) |
| Reentrancy | Mutex on wrap and claim |
| ABI / bytecode | app/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
#| Name | Type | Value | Meaning |
|---|---|---|---|
name | string | "Vellum Note" | ERC-721 collection name |
symbol | string | "VNOTE" | ERC-721 collection symbol |
MIN_TERM | uint64 | 1 days | Smallest non-zero term (86 400 s) |
MAX_TERM | uint64 | 3650 days | Largest term (315 360 000 s) |
Storage & views
#| Getter | Returns | Notes |
|---|---|---|
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) | address | Reverts Vellum: unknown note for unminted or burned ids. |
isClaimable(uint256 id) | bool | true iff the note exists and its position is unclaimed. Never reverts. |
balanceOf(address) | uint256 | Number of live notes held by the address. |
getApproved(uint256 id) | address | Per-token approval. Cleared on transfer and claim. Does not revert for unknown ids. |
isApprovedForAll(owner, op) | bool | Operator approval. |
nextTokenId() | uint256 | Id the next wrap will mint. Starts at 1. |
guardian() | address | Current guardian. |
pendingGuardian() | address | Proposed guardian awaiting acceptance, or zero. |
wrapsPaused() | bool | When true, wrap reverts. Claims and transfers are unaffected. |
supportsInterface(bytes4) | bool | ERC-165 and ERC-721 only. ERC-721 Metadata / Enumerable are not implemented. |
Core functions
#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
transferFromcall 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
TransferandNoteWrapped. Returns the new id.
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)andNoteClaimed.
Convenience read for integrators: _ownerOf[id] != 0 && !positions[id].claimed.
ERC-721 surface
#Requires to != 0, ownerOf(tokenId) == from, and that the caller is the owner, the approved address or an operator. Clears the per-token approval.
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.
Caller must be the owner or an operator for the owner. Emits Approval.
Rejects operator == msg.sender. Emits ApprovalForAll.
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
#Emergency brake for new deposits. Has no effect on existing notes, transfers or claims. Emits WrapsPauseSet.
Records pendingGuardian. Requires a non-zero address. Emits GuardianTransferProposed. The current guardian keeps its role until the proposal is accepted.
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.
| Reason | Raised by | When |
|---|---|---|
Vellum: guardian required | constructor, proposeGuardian | Zero address given for guardian |
Vellum: guardian only | setWrapsPaused, proposeGuardian | Caller is not the guardian |
Vellum: pending guardian only | acceptGuardian | Caller is not the proposed guardian |
Vellum: reentrant call | wrap, claim | Re-entered while a wrap or claim is in progress |
Vellum: unknown note | ownerOf and everything that uses it | Id never minted or already burned |
Vellum: not authorised | approve, transferFrom | Caller lacks owner/approval/operator rights |
Vellum: self approval | setApprovalForAll | Operator equals caller |
Vellum: zero recipient | transferFrom | to == address(0) |
Vellum: wrong owner | transferFrom | from is not the current owner |
Vellum: unsafe recipient | safeTransferFrom | Recipient contract returned the wrong selector |
Vellum: wraps paused | wrap | Guardian has paused deposits |
Vellum: token required | wrap | token == address(0) |
Vellum: amount required | wrap | amount == 0 |
Vellum: invalid term | wrap | term is non-zero and outside [1 day, 3650 days] |
Vellum: deposit failed | wrap | ERC-20 transferFrom reverted or returned false |
Vellum: unsupported token transfer | wrap | Vault balance did not grow by exactly amount |
Vellum: holder only | claim | Caller is not the current owner |
Vellum: already claimed | claim | Position already claimed (defensive; unreachable after burn) |
Vellum: release failed | claim | ERC-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:
| Aspect | VellumVault (production) | VellumTestVault |
|---|---|---|
| Name | Vellum Note | Vellum Test Note |
| Amount type | uint256 | uint128 |
| Minimum term | 1 day | 60 seconds |
| Position fields | token, amount, maturity, claimed | + creator (the wrapping address) |
| Exact-balance check | Yes (rejects fee-on-transfer) | No — trusts transferFrom's return value |
| Guardian / pause | Yes | None |
| safeTransferFrom | Yes | Not implemented (transferFrom only) |
| Non-boolean ERC-20s | Supported | Rejected (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.