Contract reference.

Five contracts, all deployed on Robinhood Chain. Read them yourself before you send anyone to them. That is the point of publishing the addresses.

Updated 2026-10-18 · HoodLock Team

Addresses#

ContractAddressPurpose
Locker0xd0f7d8c6e9f6d80c297bebe4f7fd1b9c8125c32fHolds ERC-20 tokens until a date
Burner0x6bf43ca706faa8ea46803299c191484e82280652Sends tokens to the dead address, on the record
Vesting0x910e19bcC4bce46999994Ed7297E0Fc4431ec72EReleases tokens gradually to a beneficiary
Airdrop0x6B1fE7b821001144Ee74EEA258b0AafdE20102E8Holds a funded airdrop that recipients claim with a Merkle proof
Liquidity Locker0x91Aa2dA1956C77F8448E18DCaf53383bCc658e67Holds a Uniswap v3 or v4 position until a date; the owner keeps collecting its fees

None of the five is upgradeable and none sits behind a proxy. We compiled the source of each one and checked that it matches the bytecode running at these addresses. You can open any address on the explorer and compare it yourself. The network parameters are on the network page.

Note

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#

FunctionNotes
lock(address token, uint256 amount, uint256 unlockTime) payableReturns 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), getLock(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. After a lock changes owner, locksByOwner still lists it under the previous owner as well, so check locks(id).owner before treating a lock as theirs.

Events#

solidity
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);
event FeeChanged(uint256 fee);

What the code guarantees#

Careful

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 and airdrop contracts do have caps; see below.

Careful

An ordinary ERC-20 works, and that is almost every token. Two narrow cases do not, and both are worth knowing before you lock.

The first is a token whose transfer returns no value at all. The locker rejects it, so the transaction fails and nothing is ever deposited. You find out immediately rather than later.

The second is a token that changes balances on its own. The locker records how many tokens arrived when you lock, and pays back exactly that number. All locks of the same token share one balance in the contract.

If a token adds to holders' balances by itself, like a reflection token or a positive rebase, the extra tokens stay in the contract and nobody can withdraw them. If a token takes away from holders' balances, like a negative rebase, the contract ends up holding less than it owes, and at least the last owner of that token will not be able to unlock.

A standard ERC-20 cannot lose tokens held by the locker, because moving or burning them needs the locker's approval, which it never gives. That is why the ordinary case is safe. The contract does not block a rebasing token, so read the token before you lock it.

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.

FunctionNotes
burn(address token, uint256 amount) payabletransferFrom(msg.sender, DEAD, amount) in one hop, so the token must be approved for the burner first. Returns the burn id. Excess ETH is refunded.
DEAD()0x000000000000000000000000000000000000dEaD
burns(id), getBurn(id), burnsByBurner(address), burnsByToken(address), totalBurns()Views.
totalBurnedOf(address token)Cumulative amount burned through this contract for one token.
solidity
event Burned(uint256 indexed id, address indexed burner, address indexed token, uint256 amount);
event FeeChanged(uint256 fee);
Irreversible

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.

Careful

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.

Careful

“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.

FunctionNotes
create(address token, address beneficiary, uint256 amount, uint64 start, uint64 cliff, uint64 end) payableRequires msg.value == fee, a non-zero beneficiary, a token with code, a positive amount, 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.
schedules(id), getSchedule(id), totalSchedules(), schedulesByToken, schedulesByBeneficiary, schedulesByCreatorViews. After a beneficiary transfer, schedulesByBeneficiary still lists the schedule under the old address too, so check schedules(id).beneficiary.

Constants#

ConstantValueWhat it protects
MAX_FEE0.05 ETHA hard ceiling the admin can never exceed.
MIN_DURATION24 hoursStops a schedule that would be fully vested on arrival.
MAX_BATCH200Bounds gas on createMany.
Careful

Vesting and airdrops take the exact fee only. Their create functions require msg.value to equal the fee exactly and have 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.

Note

Leave a margin on the 24 hours. The REST API and the embed widget check that end - start >= 86400. The contract checks end > block.timestamp + MIN_DURATION in the block where the transaction lands. A schedule of exactly 24 hours, or one with a back-dated start, can pass those checks and still revert on-chain. If you build your own UI, check against block.timestamp and add a few minutes.

solidity
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);
event FeeChanged(uint256 fee);
event FeeCollectorChanged(address collector);
event FeesWithdrawn(address indexed collector, uint256 amount);
event AdminTransferStarted(address indexed current, address indexed pending);
event AdminTransferred(address indexed previous, address indexed current);

Airdrop#

Pull-based Merkle airdrops. The creator deposits the tokens and publishes a Merkle root, and each recipient claims their own share. Nothing is pushed to a wallet. How to run one is covered in the airdrops guide.

FunctionNotes
create(address token, bytes32 merkleRoot, uint256 total, uint32 maxClaims, uint64 endTime, string uri) payableReturns the airdrop id. Requires msg.value == quote(maxClaims) exactly, a non-zero root, a positive total, between 1 and MAX_RECIPIENTS recipients, a token with code, a list URI of at most 256 bytes, and an endTime that is either 0 or at least 7 days away. The full total must arrive, so fee-on-transfer tokens are rejected.
claim(uint256 id, uint256 index, address account, uint256 amount, bytes32[] proof)Anyone can submit a claim, but the tokens always go to account. Requires a valid proof, an index not yet claimed, an airdrop that has not ended, fewer than maxClaims claims so far, and enough tokens left in this airdrop. Claiming is free.
sweep(uint256 id)Creator only, once, at or after endTime. Returns whatever nobody claimed. An airdrop created with endTime 0 can never be swept.
quote(uint32 maxClaims)The exact fee create will charge for that many recipients.
airdrops(id), getAirdrop(id), isClaimed(id, index), listURI(id), totalAirdrops()Views. There are no per-token or per-creator lists; read the AirdropCreated logs instead.

Each leaf is keccak256(bytes.concat(keccak256(abi.encode(index, account, amount)))), and pairs are hashed in sorted order. That is the OpenZeppelin convention, so proofs from standard Merkle libraries verify here.

Constants#

ConstantValueWhat it protects
MAX_FEE0.25 ETHThe most any single airdrop can be charged, however the admin sets the fee.
MIN_WINDOW7 daysA deadline has to give recipients at least a week to claim.
MAX_RECIPIENTS250,000Bounds the fee quote and the size of the list.
MAX_URI256 bytesBounds the stored list URI.
Note

The fee is feeBase + feePerWallet × maxClaims, capped at MAX_FEE. On 15 September 2026 both parameters were zero on-chain, so creating an airdrop cost only gas. Call quote before building the transaction rather than assuming it is still free.

Careful

maxClaims is a hard ceiling. The fee scales with it, and it also limits how many claims can ever succeed. Declare fewer recipients than your list holds and the last recipients will not be able to claim.

Each airdrop can only pay out its own deposit. A tree that promises more than was deposited cannot spend another airdrop's tokens; claims simply fail once this one runs out. An endTime of 0 means the airdrop stays claimable forever, and anything unclaimed stays in the contract permanently.

solidity
event AirdropCreated(uint256 indexed id, address indexed token, address indexed creator,
                     bytes32 merkleRoot, uint256 total, uint32 maxClaims, uint64 endTime, string listURI);
event Claimed(uint256 indexed id, uint256 index, address indexed account, uint256 amount);
event Swept(uint256 indexed id, address indexed creator, uint256 amount);
event FeeChanged(uint256 feeBase, uint256 feePerWallet);
event FeeCollectorChanged(address collector);
event FeesWithdrawn(address indexed collector, uint256 amount);
event AdminTransferStarted(address indexed current, address indexed pending);
event AdminTransferred(address indexed previous, address indexed current);

Liquidity Locker#

Locks a Uniswap v3 or v4 position until a date. The locker holds the position NFT for the whole lock, and the owner keeps collecting the position's trading fees while it is locked. How to use it is covered in the liquidity locker guide. The position managers it accepts are the Uniswap v3 NonfungiblePositionManager 0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3 and the Uniswap v4 PositionManager 0x58daec3116aae6D93017bAAea7749052E8a04fA7.

Functions#

FunctionNotes
lock(address manager, uint256 tokenId, uint256 unlockTime, address feeRecipient, bool acceptUnverifiedHook, uint16 maxCollectFeeBps) payableReturns the new lock id. The caller must own the position and have approved it to the locker. maxCollectFeeBps is the highest collect fee the caller accepts, so a rate change cannot land before the transaction. A v4 position in a pool with a hook that is not on the reviewed list needs acceptUnverifiedHook.
withdraw(uint256 id, address to)Only the lock's owner, only once, and only at or after unlockTime. Hands the position NFT back with one plain transfer.
extend(uint256 id, uint256 newUnlockTime)A lock can be pushed further out but never pulled in.
collect(uint256 id), collectMany(uint256[] ids)Collects the position's trading fees. The owner's share goes to the lock's fee recipient and HoodLock's share, the collect fee frozen at lock time, to HoodLock. The liquidity itself stays locked.
setFeeRecipient(uint256 id, address recipient), transferLockOwnership(uint256 id, address newOwner)Owner only. Changes where fees go, or who can withdraw. The position does not move.
lockIdOf(address manager, uint256 tokenId)The active lock id for a position, or 0 when it is not locked. The quickest way to answer “is this position locked?”
getLock(uint256 id)owner, unlockTime, collectFeeBps, isV4, hookVerified, withdrawn, feeRecipient, token0, token1, tokenId. For v4, token0 = address(0) means native ETH.
locksOf(address owner), nextLockId(), isUnlocked(uint256 id)Enumeration and views. Lock ids start at 1, so ids 1..nextLockId-1 all exist. locksOf keeps a lock listed under a previous owner too, so check getLock(id).owner.

Events#

solidity
event Locked(uint256 indexed id, address indexed owner, address indexed manager, uint256 tokenId,
    address token0, address token1, bytes32 pool, uint256 unlockTime, uint16 collectFeeBps,
    bool hookVerified, address feeRecipient, uint256 fixedFeePaid);
event Extended(uint256 indexed id, uint256 newUnlockTime);
event Withdrawn(uint256 indexed id, address indexed to);
event LockOwnershipTransferred(uint256 indexed id, address indexed from, address indexed to);
event FeeRecipientChanged(uint256 indexed id, address indexed recipient);
event Collected(uint256 indexed id, address indexed recipient, uint256 amount0, uint256 amount1,
    uint256 cut0, uint256 cut1, bool byAdmin);

In Collected, amount0 and amount1 are everything the position paid out, before HoodLock's cut. The fee recipient received amount0 - cut0 and amount1 - cut1.

pool in Locked is the v4 pool id, or the v3 pool address left-padded to 32 bytes.

What the code guarantees#

Integrating#

Explorers, scanners and security tools can tell whether a pool's liquidity is locked with two reads:

Every lock has a public proof page at https://hoodlock.tech/proof/position/{lockId}, and the same data as JSON from GET /api/lp/lock/{lockId}; GET /api/lp/locks lists every liquidity lock with its pair, amounts and value. The REST API covers the rest.

Administration#

All five contracts share one admin address, 0x79c1230cAb12d53D040f5FE1F5279e1A481CCeA2. It is a single wallet, not a multisig. Fees go to the fee collector, which is a contract at 0xd59948E49B1B56784626bDe2d6D1712eD24a6BBc. No admin function reads or moves user tokens, and none can change an existing lock, burn record, schedule or funded airdrop.

ContractWhat the admin can changeAdmin handover
LockerThe fee, with no cap, and the fee collector.setAdmin, one step, effective immediately.
BurnerThe fee, with no cap, and the fee collector.setAdmin, one step, effective immediately.
VestingThe fee, up to 0.05 ETH, and the fee collector.transferAdmin, then acceptAdmin by the new admin.
AirdropThe two fee parameters, each up to 0.25 ETH, and the fee collector. A single airdrop never pays more than 0.25 ETH.transferAdmin, then acceptAdmin by the new admin.
Liquidity LockerThe collect fee and fixed fee for new locks, within limits written into the contract; where HoodLock's share is sent; pausing new locks; the reviewed-hook list. An existing lock keeps the rate it was created with.transferAdmin, then acceptAdmin by the new admin.

The $LOCK buyback and burn uses separate contracts, which are not covered on this page.

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.