Contract reference.
Three contracts, all deployed on Robinhood Chain and all verified on Blockscout. Read them yourself before you send anyone to them — that is the point of publishing the addresses.
Addresses#
| Contract | Address | Purpose |
|---|---|---|
| Locker | 0xd0f7d8c6e9f6d80c297bebe4f7fd1b9c8125c32f | Holds ERC-20 tokens until a date |
| Burner | 0x6bf43ca706faa8ea46803299c191484e82280652 | Sends tokens to the dead address, on the record |
| Vesting | 0x910e19bcC4bce46999994Ed7297E0Fc4431ec72E | Releases tokens gradually to a beneficiary |
All three are verified, so the source you read on Blockscout is the bytecode that runs. The network parameters are on the network page.
None of these contracts has a function that lets us move your tokens. That is a property of the code, not a promise — the section on each contract below points at the specific functions to check.
Locker#
One vault, many locks. Each lock records an owner, a token, an amount and an unlock time.
Functions#
| Function | Notes |
|---|---|
lock(address token, uint256 amount, uint256 unlockTime) payable | Returns the new lock id. Requires msg.value >= fee, a non-zero token, a positive amount and a future unlock time. Excess ETH is refunded. |
withdraw(uint256 id) | Only the lock's owner, only once, and only at or after unlockTime. |
extend(uint256 id, uint256 newUnlockTime) | Requires newUnlockTime > unlockTime. A lock can be pushed further out but never pulled in. |
transferLockOwnership(uint256 id, address newOwner) | Moves the right to withdraw. The tokens do not move. |
locks(id), lockedAmount(id), isUnlocked(id), timeRemaining(id) | Views. lockedAmount returns 0 once withdrawn. |
locksByOwner(address), locksByToken(address), totalLocks() | Enumeration, used by the explorer and by proof pages. |
Events#
event Locked(uint256 indexed id, address indexed owner, address indexed token, uint256 amount, uint256 unlockTime);
event Withdrawn(uint256 indexed id, address indexed owner, uint256 amount);
event Extended(uint256 indexed id, uint256 newUnlockTime);
event LockOwnershipTransferred(uint256 indexed id, address indexed from, address indexed to);What the code guarantees#
- Locked tokens can only be withdrawn by the lock's owner, and only at or after the unlock time.
- There is no admin function that can move locked tokens.
unlockTimecan only ever be extended.- The amount recorded is measured as the balance the contract actually gained, so a fee-on-transfer token is not over-credited.
The locker has no hard cap on the fee. The admin can change it, and a change applies to new locks only — an existing lock is never re-priced. The vesting contract does have a cap; see below.
Balance-delta accounting has a blind spot. It measures what the contract gained, which is not the same as what you sent. On a reflection token — where holders are credited during other people's transfers — a deposit can be recorded above or below what actually left your wallet, because rewards earned by the existing pool land inside the same measurement window.
There is also one balance pool per token, not one per lock. If a token's balances can move without a transfer — a negative rebase, an owner burn, a blacklist zeroing — the pool can end up smaller than the sum of what it owes. Withdrawal is all-or-nothing and first-come-first-served, so the shortfall falls entirely on whoever withdraws last. Rebasing and reflection tokens are not supported, and nothing in the contract enforces that.
Burner#
A burn registry rather than a vault. Tokens are moved from your wallet to the dead address in a single transfer and never rest in the contract.
| Function | Notes |
|---|---|
burn(address token, uint256 amount) payable | transferFrom(msg.sender, DEAD, amount) in one hop. Returns the burn id. Excess ETH is refunded. |
DEAD() | 0x000000000000000000000000000000000000dEaD |
burns(id), burnsByBurner, burnsByToken | Views. |
totalBurnedOf(address token) | Cumulative amount burned through this contract for one token. |
event Burned(uint256 indexed id, address indexed burner, address indexed token, uint256 amount);Nothing in this contract can move tokens back out of the dead address, and no key controls that address. From HoodLock's side a burn is final. Read burning versus locking before choosing it.
The token can still undo it. Sending to the dead address parks tokens there; it does not
reduce totalSupply. A token that is mintable can re-issue the same amount, and an upgradeable
token can add a function that moves the dead address's balance. Neither is detectable from here.
The same limitation applies to totalBurnedOf: it is derived from the token's own
balanceOf readings, so a token that reports whatever it likes produces a burn record that
looks legitimate. Check whether the token is mintable or upgradeable before treating a burn total as
supply reduction.
Vesting#
Linear release with an optional cliff. There is no revoke, no sweep and no rescue function in this contract, so no HoodLock function can cancel or alter a schedule once it exists.
“Irrevocable” describes this contract, not the token. A token that is upgradeable, pausable, mintable, or that can blacklist an address, can still make a schedule worthless or unclaimable after the fact, and the schedule's creator is very often the token's deployer, which means they may retain exactly that power.
The reverse also holds: because there is no rescue path, a token that is paused or that freezes the beneficiary locks the tokens permanently, with no recourse from anyone. Irrevocable and unrecoverable are the same property.
| Function | Notes |
|---|---|
create(address token, address beneficiary, uint256 amount, uint64 start, uint64 cliff, uint64 end) payable | Requires start <= cliff <= end, end > start, and end > block.timestamp + MIN_DURATION. |
createMany(...) | Batch, up to MAX_BATCH beneficiaries. Requires msg.value == fee * n. |
claim(uint256 id) | Beneficiary only. Claiming is free — the fee is charged once, at creation. |
claimable(uint256 id) | 0 before the cliff; linear between cliff and end; the exact remainder at end, so nothing is left as dust. |
transferBeneficiary(uint256 id, address newBeneficiary) | Current beneficiary only. |
Constants#
| Constant | Value | What it protects |
|---|---|---|
MAX_FEE | 0.05 ETH | A hard ceiling the admin can never exceed. |
MIN_DURATION | 24 hours | Stops a schedule that would be fully vested on arrival. |
MAX_BATCH | 200 | Bounds gas on createMany. |
The exact-fee rule differs from the other two contracts. create requires
msg.value == fee exactly and has no refund path, while the locker and burner accept an
overpayment and refund the difference. Read the fee immediately before building the transaction rather
than caching it.
A validation gap worth knowing about. The REST API checks that
end - start >= 86400, but the contract checks end > block.timestamp +
MIN_DURATION. A schedule with a back-dated start can satisfy the API and still revert
on-chain. If you build your own UI, check against block.timestamp, not against
start.
event VestingCreated(uint256 indexed id, address indexed token, address indexed beneficiary,
address creator, uint256 total, uint64 start, uint64 cliff, uint64 end);
event Claimed(uint256 indexed id, address indexed beneficiary, uint256 amount);
event BeneficiaryTransferred(uint256 indexed id, address indexed from, address indexed to);Administration#
Across all three contracts the admin can change only the fee, the fee collector and the admin key. No admin function reads or moves user tokens.
- Locker and burner forward the fee per transaction. Vesting accrues fees in-contract and the collector pulls them with
withdrawFees(). - Vesting uses a two-step admin transfer —
transferAdminthenacceptAdmin, so a mistyped address cannot strand the role.
How to confirm any of this yourself is covered in the security model, and reading a contract on the explorer is covered in this walkthrough.