# Hood Locker
An immutable token, NFT and liquidity locker on Robinhood Testnet (chain id 46630).
Assets are locked until a date the locker chooses. Nobody — including the contract's owner —
can release them early. There is no such function.
Site: https://onchainhoodies.locker
## Everything you need, in one place
- `GET /api/abi` — the full ABI plus addresses, chain id and RPC. Fetch this first; it is the
only thing here you cannot derive from the chain, because you need it to encode a call.
- `GET /api/locks?token=0x…` or `?owner=0x…` — what is locked, as JSON. Add
`&chainId=`/`&locker=` to pin the deployment and get a 409 rather than a wrong answer.
- `/contract` — the full verified source of the deployed contract, as text.
- Everything below is also readable directly from the chain, and you should prefer that.
## Addresses
- Locker: 0x6a249F0c1173d68b0Dd6a7Eb265315a6B0707cC5 — on chain 46630. This is
the one value you cannot verify by reading the contract, because you need it to read the
contract. Confirm it before you send anything: `cast code
--rpc-url ` must
return bytecode, and `feeToken()` must return the $OCH address you expect.
- Hoodie collection — `hoodieNft()` on the locker. **Locking one of these is free**, and doing
so also freezes its ERC-6551 wallet, because authority there follows the current NFT owner and
this contract cannot sign: 0xCBDEE2b95C4F02Bf05A1E3E0dcec79495Eb0e83E
- Liquidity position managers: 0xa6edfd51c98d936598db2adc9508570d177f0c0d
A Uniswap position is an ERC-721, so locking one is an ordinary `lockNFT` call — the contract
has no concept of an "LP lock" and no whitelist. This list is only how the website labels such
locks; locking a position manager that isn't listed works exactly the same.
- RPC: https://rpc.testnet.chain.robinhood.com
## Live state
- Fee per lock: 2,500 $OCH
- Locks created, all time: 1
Read these yourself rather than trusting this file: `feeAmount()`, `lockBps()`, `devBps()`,
`feeLockDuration()`, `locksCount()` are all public.
## How to lock
Two transactions. Both are ordinary ERC-20/ERC-721 flows.
**ERC-20 (a token, or a project's own supply):**
1. `approve(locker, amount)` on the token.
2. `lock(address token, uint256 amount, uint256 unlockTime, address beneficiary, uint256 maxFee)`
**ERC-721 (NFTs, and liquidity positions — a Uniswap v3/v4 position is an ERC-721):**
1. `setApprovalForAll(locker, true)` on the collection.
2. `lockNFT(address collection, uint256[] tokenIds, uint256 unlockTime, address beneficiary, uint256 maxFee)`
`unlockTime` is a unix timestamp in **seconds**, and must be in the future. Both functions
return the new lock's id, and also emit `Locked(uint256 indexed lockId, ...)`.
**Lock ids are not contiguous with your own locks.** When a fee is paid, `lockBps` of it is
locked back into the contract as a real lock owned by the treasury — so paying a fee consumes an
id before yours. `locksCount()` counts those too. Don't assume your two locks are ids 0 and 1,
and don't read `locksCount()` as "how many locks users made".
### The three things that trip people up
1. **`maxFee` is not optional.** It is the most $OCH you agree to pay for this lock. Pass the
value `feeFor(yourAddress)` returns. If you pass less than the live fee the call reverts —
that is the point: a fee change can never overcharge a lock already in flight.
2. **The fee depends on what you are locking, not on who you are.** Locking a hoodie is free;
everything else costs `feeAmount`. Holding a hoodie is not a discount. Call
`feeForAsset(address)` with the asset you are locking rather than assuming.
3. **`beneficiary` is permanent.** It is the only address that will ever be able to withdraw,
it is fixed at creation, and locks are not transferable. Getting it wrong is unrecoverable.
Paying the fee also needs `approve` on $OCH first — the fee is pulled with `transferFrom`.
If the asset being locked IS $OCH, approve `amount + fee` in one go, since ERC-20 approvals are
absolute and a second approve would overwrite the first.
## Getting your assets back
- `withdraw(uint256 lockId)` — sends everything back to the lock's beneficiary. Callable only by
that address, and only once `block.timestamp >= unlockTime`. Reverts before then. Nothing is
automatic: a lock past its date sits in the contract until somebody calls this.
- `extend(uint256 lockId, uint256 newUnlockTime)` — pushes the date out. The new time must be
later than the current one; there is no way to bring it forward.
## Reading what's locked
Without any indexer, straight from the contract:
- `locks(uint256 lockId)` / `getLock(uint256 lockId)` — one lock by id
- `getLockTokenIds(uint256 lockId)` — which NFT ids an ERC-721 lock holds. The only way to find
this out; the lock struct records the count, not the ids.
- `getLocksByToken(address token, uint256 offset, uint256 limit)` — every lock on a token
- `getLocksByOwner(address owner, uint256 offset, uint256 limit)` — every lock a wallet holds
- `feeForAsset(address asset)` — the fee to lock that asset right now. 0 for the hoodie
collection, `feeAmount` for everything else.
- `feeToken()` — the ERC-20 fees are paid in. Check it matches the $OCH you think you're using.
Every lock reads as `(address token, address owner, uint256 amount, uint256 unlockTime,
uint8 kind, bool withdrawn)`:
- `owner` is the beneficiary — the only address that can withdraw.
- `kind` is 0 for ERC-20, 1 for ERC-721.
- `amount` is the token amount for ERC-20 locks, **and the NFT count for ERC-721 locks** — so
`totalLocked()` on a collection is a count of NFTs, not an amount.
- `withdrawn` is whether it has already been claimed. Past its date but `withdrawn == false`
means still held and claimable at any moment — which is not the same as committed.
- `totalLocked(address token)` — currently-locked amount, or NFT count
- `activeLockedOf(address who, address token)` — how much that wallet has locked and unmatured
- `hasCommitment(address who, address token, uint256 minAmount, uint256 minUnlock)` — a yes/no
gate any contract or bot can use, e.g. to check a project really has locked what it claims
Pages are capped at `MAX_PAGE` (500). Pass offset/limit and page through.
A JSON view of the same data, if that's easier than RPC:
GET /api/locks?token=0x… every lock on a token
GET /api/locks?owner=0x… every lock a wallet holds
## What the owner can and cannot do
Can, and only for future locks: the fee amount, the treasury, how each fee splits between
locking / treasury / dev, and how long the locker's own fee-locks are held.
Cannot, ever: touch a lock. There is no early-release function, no upgrade path, no proxy. The
fee is capped at 25,000 $OCH by a constant, and the dev
share at 5%. Neither can be raised by anyone.
Unlock dates can be extended, never shortened.
## If someone has asked you to lock something for them
The order that works, and the things that bite:
1. `GET /api/abi` for the ABI and addresses. Confirm with `cast code` that the locker address
holds bytecode on the chain id you were given.
2. `feeForAsset(assetYouAreLocking)` — this is the fee. It is 0 only when the asset is the
hoodie collection. Use the value it returns as `maxFee`; do not guess, and do not pass 0
unless that is what it returned.
3. If the fee is non-zero, `approve` that amount of `feeToken()` to the locker first. This is a
separate token from the one being locked, and forgetting it is the usual first failure.
4. `approve` (ERC-20) or `setApprovalForAll` (ERC-721) for the asset itself.
5. `lock` / `lockNFT`. Check the `beneficiary` argument with whoever asked you — it is
permanent, it is the only address that will ever be able to withdraw, and there is no
recovery if it is wrong.
6. Read the lock back with `getLocksByOwner` before reporting success.
Locking is irreversible until the date given. If any part of the instruction is ambiguous —
especially the amount, the date, or the beneficiary — ask before sending, not after.
## Caveats worth knowing
- Rebasing tokens are unsupported — amounts are recorded at lock time, so a token that changes
balances afterwards will strand or misallocate funds.
- Fee-on-transfer tokens work: the amount actually received is what gets recorded.
- Up to 50 NFTs per `lockNFT` call.
- A lock past its unlock date stays in the contract until the owner withdraws it. "Unlocked" and
"withdrawn" are different states, and the difference matters when judging whether supply is
still committed.