Security
What the Virtuallock team can do with your locked tokens: nothing.
Every claim on this page points to the contract, its tests or a public source. Where something isn't done yet, it says so.
Trust model
TokenLock is zero-admin. Nobody has special powers over it, including us.
- No owner. TokenLock has no owner, admin or role-based access control, so there is no privileged key to lose or steal.
- No pause. No function can stop locks, withdrawals or extensions.
- No upgrade. It isn't deployed behind a proxy. The code at the address is the code that runs, permanently.
- Fixed fee. The flat fee and the fee recipient are immutable, set once when the contract was deployed.
- Extend-only. An unlock time can be moved later by the depositor. Any attempt to move it earlier reverts.
- Only the depositor withdraws, only after maturity. Withdraw reverts for any other caller and for any call before the unlock time. Each lock pays out once.
- No migrate, rescue or sweep. There is no function that moves locked tokens anywhere except back to the depositor (and the chosen burn share to the dead address) on withdrawal. Plain ETH sent to the contract is rejected.
Every function, and who can call it
createLock(token, amount, unlockTime, burnBps)- Anyone
- Pulls the amount from the caller, records the lock with the caller as depositor, and forwards the exact flat fee. The unlock time must be in the future; the burn share is at most 100%.
withdraw(lockId)- The lock's depositor, after the unlock time
- Sends the tokens to the depositor, minus the burn share, which goes to 0x…dEaD. Works once per lock.
extendLock(lockId, newUnlockTime)- The lock's depositor
- Moves the unlock time later. Reverts if the new time isn't later than the current one.
getLock, getUserLockIds, flatFee, feeRecipient, nextLockId, MAX_BURN_BPS, BURN_ADDRESS- Anyone (read-only)
- Return lock data, the fixed fee settings and constants. They change nothing.
receive()- Nobody
- Always reverts, so ETH can't be sent to the contract directly.
What a lock does and doesn't do
A lock proves that specific tokens can't move until a date. It doesn't make a token or a project trustworthy.
It does
- No one, including the Virtuallock team, can move locked tokens before the unlock time.
- No one can shorten a lock or change its burn share after it's created.
- The fee for a lock can't be raised: it's fixed in the contract.
- Anyone can check a lock on-chain, without trusting this website.
It doesn't cover
- Unlocked supply. A lock covers only the tokens deposited. Team, insider or other wallets holding unlocked tokens can still sell them.
- After maturity. Once a lock unlocks, the depositor can withdraw and do anything with the tokens.
- The token's own contract. If a token has an owner who can mint, pause transfers, blacklist wallets or change transfer fees, locking it doesn't remove those powers.
- Tokens that change balances on their own. TokenLock records what actually arrived, but a rebasing token that later shrinks balances can leave a lock unable to pay out in full.
- The depositor's keys. Whoever controls the depositor wallet controls withdraw and extend. Locks can't be transferred or recovered.
- Undiscovered bugs. The contracts have not been audited by a third party.
- This website and the chain. Check that the contract you approve matches the address below and on the explorer. Locks also depend on Robinhood Chain operating normally.
Contract addresses
The contracts this site uses on Robinhood Chain. Compare them with what your wallet asks you to approve.
- TokenLockTime locks with optional burn on exit
0x15B9B92F59C0ecFfBE4F200E35D13cEEF1bCbaE4 - VestingVaultCliff and linear vesting
0x6b885b0b4fc34cbd35ca55d083613087c0bc4295 - BurnRouterProvable burnsNot deployed on this network yet
This is the production deployment. The testnet deployment lists its own addresses on its own site.
Read the source
After a lock is created, these two functions are the only ones that can change it. Copied from the contract when this site was built.
function withdraw(uint256 lockId) external nonReentrant {
LockInfo storage lock = _locks[lockId];
if (lock.owner == address(0)) revert LockNotFound();
if (msg.sender != lock.owner) revert NotLockOwner();
if (lock.withdrawn) revert AlreadyWithdrawn();
if (block.timestamp < lock.unlockTime) revert LockNotMatured();
lock.withdrawn = true;
uint256 amount = lock.amount;
uint256 burnAmount = (amount * lock.burnBps) / BPS_DENOMINATOR;
uint256 returnAmount = amount - burnAmount;
IERC20 token = IERC20(lock.token);
if (burnAmount > 0) token.safeTransfer(BURN_ADDRESS, burnAmount);
if (returnAmount > 0) token.safeTransfer(msg.sender, returnAmount);
emit LockWithdrawn(lockId, msg.sender, returnAmount, burnAmount);
}
Reverts unless the caller is the depositor, the lock hasn't been withdrawn, and the unlock time has passed.
function extendLock(uint256 lockId, uint64 newUnlockTime) external {
LockInfo storage lock = _locks[lockId];
if (lock.owner == address(0)) revert LockNotFound();
if (msg.sender != lock.owner) revert NotLockOwner();
if (lock.withdrawn) revert AlreadyWithdrawn();
if (newUnlockTime <= lock.unlockTime) revert UnlockTimeCannotDecrease();
lock.unlockTime = newUnlockTime;
emit LockExtended(lockId, newUnlockTime);
}
The only function that changes an unlock date. A new date at or before the current one reverts.
packages/contracts/src/TokenLock.solWhy zero-admin matters
Each of these losses came through something beyond lock and withdraw: an owner key, a migration function, approvals left behind by a cancel feature. TokenLock has none of them.
DxSale v1 liquidity locker May 2026
The owner key of a locker contract deployed in 2021 ended up with an attacker, who used it to unlock about 1,400 liquidity locks on BNB Chain. Reported losses were about $7.3M. DxSale said its v2 and later lockers were not affected.
In TokenLock: TokenLock has no owner key at all, so there is no key that could unlock anything.
Team Finance October 2022
A flaw in a function for migrating locked Uniswap v2 liquidity to v3 let an attacker move locked liquidity. Reported losses were about $15.8M.
In TokenLock: TokenLock has no migrate or convert function. Locked tokens only ever leave through withdraw, to the depositor, after maturity.
Sources:The Block (opens in a new tab)Hedgey Finance April 2024
Token approvals left in place after a claim campaign was cancelled were used to take tokens. Headline losses were reported at about $44.7M.
In TokenLock: TokenLock has no campaigns, claims or cancellation. It only pulls tokens from the wallet that calls createLock, in that same call.
Figures are as reported by the linked sources at the time. For a sourced, dated comparison of lockers in use today, see Token lockers compared.
Tests and static analysis
Every change to the contracts runs these checks in CI before it can merge.
- Unit tests for TokenLock. Creating locks, withdrawing before and after maturity, calls from anyone but the depositor, double withdrawal, extend-only dates, fee-on-transfer tokens, and a fee recipient that tries to re-enter.
packages/contracts/test/TokenLock.t.sol - Fuzz testing. Burned plus returned amounts always add up to the locked amount, across random burn shares and durations. CI runs every fuzz test 1,000 times.
packages/contracts/test/TokenLock.t.sol - Invariant tests for BurnRouter. Stateful tests check that the router never holds tokens, that its ETH always equals partners' balances, and that fees are conserved.
packages/contracts/test/BurnRouter.t.sol - Slither static analysis. Runs on every pull request that touches the contracts and must pass before merge.
.github/workflows/contracts-ci.yml - Manual approval for production deploys. A production contract deploy waits for a person to approve it in a protected GitHub Environment.
.github/workflows/contracts-deploy.yml
The repository isn't public yet, so these paths aren't links. The deployed contracts' source is verified on the explorer (see Contract addresses).
Audit
Virtuallock's contracts have not been audited by a third party. Reports go through the bug bounty.
Until an audit is published, the Virtuallock app limits new locks to $50,000 per token and $25,000 per lock. This limit is in the app only: the contract itself has no cap, and calling it directly bypasses the limit.
Report a vulnerability
Please don't post a vulnerability publicly. Send a description of the issue and its impact, steps to reproduce (a failing Forge test is ideal) and any suggested fix. You'll get an acknowledgment within 48 hours.
A dedicated private reporting address will be published here before the mainnet launch.
The full policy is in SECURITY.md.
Bug bounty
A critical bug earns 10% of the funds it puts at risk, up to a maximum set by the maintainer.
This is a self-hosted programme, run from SECURITY.md.
Rewards
- Critical
Theft or permanent loss of locked or vesting tokens, or of ETH held by the contracts; tokens released before maturity or to anyone but the depositor or beneficiary; an unlock time shortened; anyone gaining control over another user's lock.
Reward: 10% of the funds directly at risk, up to $10,000.
- High
Funds frozen without being lost: a withdrawal, claim or burn that can be blocked for a user, or fees or partner shares paid to the wrong party or in the wrong amount.
Reward: Up to $2,500.
- Medium
The contract breaks a documented promise without putting funds at risk: events that misreport what happened, griefing that only costs gas, a documented check that can be bypassed with no loss.
Reward: Up to $500.
- Low
Minor or informational issues with a concrete, if small, impact.
Reward: Public thanks and credit, if you want it.
In scope
The contracts at the addresses this site uses, once deployed to a public network:
- TokenLock
0x15B9B92F59C0ecFfBE4F200E35D13cEEF1bCbaE4Explorer (opens in a new tab) - VestingVault
0x6b885b0b4fc34cbd35ca55d083613087c0bc4295Explorer (opens in a new tab) - BurnRouter (in scope once deployed; not deployed on this network yet)
A bug shown on a fork or a testnet counts. Rewards are worked out from mainnet funds at risk when you report it. Before the mainnet launch, valid findings are credited and any reward is at the maintainer's discretion.
Out of scope
- This website, the public API, the indexer, the back office and the SDK. Please still report problems in them; they just aren't covered by rewards.
- Contracts that aren't deployed to a public network yet.
- Limits already documented in SECURITY.md, docs/VESTING.md or docs/BURN_ROUTER.md, such as a burner naming their own wallet as BurnRouter partner.
- Behaviour of the locked token itself (fee-on-transfer, rebasing, blocklists, pausable or malicious tokens) that only affects locks of that token.
- Robinhood Chain itself: sequencer downtime, reorgs, RPC or explorer outages.
- Attacks that need a user's private key, a compromised wallet, phishing or social engineering.
- Gas optimisations, style or best-practice notes without a concrete impact.
- Bugs in third-party libraries such as OpenZeppelin, unless they can be exploited through our contracts.
Rules
- Test on a local fork or a testnet. Never touch mainnet funds that aren't yours.
- Don't disclose the issue publicly until we confirm users have been told and, where needed, a redeployment is available, or until 90 days after your report, whichever comes first.
- Do only what you need to prove the issue, and don't degrade the service for others.
- Threats, or demands for payment as a condition of disclosure, void any reward.
Safe harbour
If you make a good-faith effort to follow these rules, we'll treat your research as authorised, we won't take or support legal action against you for it, and we'll work with you to understand and fix the issue quickly.
How to report
Use the private route under Report a vulnerability. A dedicated reporting address will be published here before the mainnet launch.