Locked liquidity, and how to check it yourself
When a Vectr token graduates, its liquidity becomes a Uniswap V3 position that a contract holds with no way to give it back. This guide is what that contract can do, what it cannot, where the fees go, and the calls and events that let you confirm the lock without trusting this page. Addresses are read from GET /config at render time.
Updated
What is locked, and by what
Uniswap V3 represents a liquidity position as an NFT issued by its NonfungiblePositionManager. Whoever owns that NFT, or an address the owner approves, can withdraw the liquidity. At graduation the migrator puts the whole raise and the 200M reserved tokens into one full-range position and mints its NFT straight to VectrLPLocker. "Locked" means that owner is a contract with no code path that moves, unwinds or approves it. It is not a promise with an expiry date.
- 01Curve
_graduate()Hands the raise and the remaining tokens to the migrator it was created with.
- 02Migrator
migrate()Creates the Uniswap V3 pool and prices it where the curve ended.
- 03Position manager
mint(...)Mints one full-range position NFT with the locker as recipient.
- 04VectrLPLocker
lock(tokenId, ...)Records it. From here the NFT has no way out.
- VectrLPLocker
- 0xcf489e55a1167Cf883e0EE43D2354fdFc82b2B58 Holds every graduation position. No owner, no proxy
- VectrUniswapMigrator
- 0x384EEd2Ba540146FB09E4EE228B454091913F837 Deployed the locker in its constructor; the only address that can call lock()
- NonfungiblePositionManager
- 0x73991a25c818bf1f1128deaab1492d45638de0d3 Uniswap's contract, where the position and its ownership live
- Pool fee tier
- 1.00%tick spacing 200; full range is -887200 to 887200
Every function the locker has
The whole external interface of VectrLPLocker. Nothing else can be called on it.
| Function | Who can call it | What it does |
|---|---|---|
lock(tokenId, token, token0, token1) | Only the migrator, an immutable address set when the locker was deployed | Records a position the locker already owns. Reverts unless ownerOf(tokenId) is the locker, and allows one position per token. |
collect(tokenId) | Anyone | Pulls the fees the position has earned and forwards them through the FeeCollector to the platform wallets. Reverts for a position it did not lock. |
collectMany(tokenIds) | Anyone | collect() for several positions in one transaction. |
tokenPosition, positions, positionToken, positionCount, positionAt | Anyone (view) | The registry: which position belongs to which token, and every position locked so far. |
positionManager, feeCollector, weth, migrator | Anyone (view) | The wiring, fixed at deployment. |
onERC721Received | The position manager, on mint | Accepts the NFT. It changes nothing. |
receive() | WETH only | Accepts ETH only from WETH's withdraw, which collect() uses to unwrap fees, and forwards it in the same call. |
lock(tokenId, token, token0, token1)- Who can call it
- Only the migrator, an immutable address set when the locker was deployed
- What it does
- Records a position the locker already owns. Reverts unless ownerOf(tokenId) is the locker, and allows one position per token.
collect(tokenId)- Who can call it
- Anyone
- What it does
- Pulls the fees the position has earned and forwards them through the FeeCollector to the platform wallets. Reverts for a position it did not lock.
collectMany(tokenIds)- Who can call it
- Anyone
- What it does
- collect() for several positions in one transaction.
tokenPosition, positions, positionToken, positionCount, positionAt- Who can call it
- Anyone (view)
- What it does
- The registry: which position belongs to which token, and every position locked so far.
positionManager, feeCollector, weth, migrator- Who can call it
- Anyone (view)
- What it does
- The wiring, fixed at deployment.
onERC721Received- Who can call it
- The position manager, on mint
- What it does
- Accepts the NFT. It changes nothing.
receive()- Who can call it
- WETH only
- What it does
- Accepts ETH only from WETH's withdraw, which collect() uses to unwrap fees, and forwards it in the same call.
The functions it does not have
Uniswap's position manager lets only the owner, or an address the owner approved, transfer a position or take liquidity out of it. The locker is the owner and has no function that does either, or that approves anyone who could.
- transferFrom
- safeTransferFrom
- approve
- setApprovalForAll
- decreaseLiquidity
- burn
- withdraw
- rescue
- sweep
// test/uniswap-graduation.test.ts: the locker's compiled ABI, not a comment, is what is checked
const fns = locker.interface.fragments.filter((f) => f.type === 'function').map((f) => f.name);
for (const banned of ['transferFrom', 'safeTransferFrom', 'approve', 'setApprovalForAll',
'decreaseLiquidity', 'burn', 'withdraw', 'rescue', 'sweep']) {
assert.ok(!fns.includes(banned), `locker exposes ${banned}`);
}The same test calls transferFrom and decreaseLiquidity on the real Uniswap position manager from the deployer, the token's creator and an unrelated account, and requires each call to revert. The locker has no owner and no upgrade path: it is deployed with new VectrLPLocker(...) inside the migrator's constructor, so its code is fixed for good.
collect() moves fees, never principal
VectrLPLocker.collect(tokenId) // anyone can call it
-> NonfungiblePositionManager.collect(tokenId, recipient = locker, max, max)
-> WETH side: WETH.withdraw, then FeeCollector.depositPlatformFee{value}
-> ERC-20 side: transfer to FeeCollector, then depositPlatformFeeToken(asset, amount)
-> FeeCollector pushes it to the platform wallets by share, in the same transaction
emit FeesCollected(tokenId, token, token0, amount0, token1, amount1)The pool charges 1.00% on every swap, and that fee accrues to the position. collect() asks Uniswap for everything the position is owed and passes it on; since nothing can ever decrease the position's liquidity, what it is owed is fees. The locker keeps no balance: the tests check it holds no WETH and no ETH after a sweep. For a Stock Token pair the Stock Token side is paid out as the Stock Token, unconverted; see Stock Token pairing.
- Platform wallet 1
- 0xe82073dd4ee10d4fe94a48f3a3252f41a02f642e 40% of every platform fee, read from the FeeCollector
- Platform wallet 2
- 0x6a062c4e8a7b6d773174259b7804313f867ba538 40% of every platform fee, read from the FeeCollector
- Platform wallet 3
- 0xce1176b9333730eb40cfb85ee1fdbecb770f4725 20% of every platform fee, read from the FeeCollector
Check a graduated token yourself
Six read-only checks. None needs a key, and every answer comes from the chain, not from Vectr's API.
- 1Find the curveAsk the factory for the token's curve and confirm it has graduated.
factory.getTokenCurve(token) -> curve; curve.graduated() -> true - 2Find its migrator and positionA curve records its migrator once, when it is created. Ask that migrator for the position and for its locker.
curve.liquidityMigrator() -> migrator; migrator.tokenToPosition(token) -> tokenId; migrator.locker() -> locker - 3Ask the lockerThe locker should name the same migrator and the same position.
locker.migrator() -> migrator; locker.tokenPosition(token) -> tokenId - 4Ask Uniswap who owns itThe position manager is Uniswap's own contract, not Vectr's. It should report the locker as owner and no approved operator.
positionManager.ownerOf(tokenId) -> locker; positionManager.getApproved(tokenId) -> 0x0000...0000 - 5Check the range and the liquiditypositions(tokenId) should show the graduation fee tier, ticks -887200 to 887200 (full range), and liquidity no lower than the figure in the migrator's PositionLocked event.
positionManager.positions(tokenId) -> fee 10000, tickLower -887200, tickUpper 887200, liquidity - 6Read the historyThe graduation transaction carries the curve's Graduated, the migrator's PoolCreated and PositionLocked, the locker's PositionLocked, and the position manager's Transfer from the zero address to the locker. After it, the only position-manager events for that tokenId are Collect, paid to the locker.
DecreaseLiquidity(tokenId, ...) -> none, ever; Transfer(locker -> anyone, tokenId) -> none, ever
The same checks with Foundry's cast, using the live addresses:
RPC=https://rpc.mainnet.chain.robinhood.com
FACTORY=0xB2f232389a4EE9285ceeE5E7c1706a0484437873
NPM=0x73991a25c818bf1f1128deaab1492d45638de0d3
LOCKER=0xcf489e55a1167Cf883e0EE43D2354fdFc82b2B58
TOKEN=0x... # the graduated token
# 1. Set CURVE to the first result; the second prints true
cast call $FACTORY "getTokenCurve(address)(address)" $TOKEN --rpc-url $RPC
cast call $CURVE "graduated()(bool)" --rpc-url $RPC
# 2. Set MIGRATOR, then ID; the last prints $LOCKER
cast call $CURVE "liquidityMigrator()(address)" --rpc-url $RPC
cast call $MIGRATOR "tokenToPosition(address)(uint256)" $TOKEN --rpc-url $RPC
cast call $MIGRATOR "locker()(address)" --rpc-url $RPC
# 3. Prints $ID
cast call $LOCKER "tokenPosition(address)(uint256)" $TOKEN --rpc-url $RPC
# 4. Owner is $LOCKER; approved is the zero address
cast call $NPM "ownerOf(uint256)(address)" $ID --rpc-url $RPC
cast call $NPM "getApproved(uint256)(address)" $ID --rpc-url $RPC
# 5. Fields 5 to 8: fee, tickLower, tickUpper, liquidity
cast call $NPM "positions(uint256)(uint96,address,address,address,uint24,int24,int24,uint128,uint256,uint256,uint128,uint128)" $ID --rpc-url $RPC
# 6. No output: no DecreaseLiquidity for this position, ever
cast logs --rpc-url $RPC --from-block <graduation block> --address $NPM \
"DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)" $IDIn a browser, open the graduation transaction's logs and the position manager's read functions on robinhoodchain.blockscout.com (Blockscout, the explorer Robinhood lists) or robin.etherscan.io (RobinScan, where Vectr's contracts are verified). The event signatures and their topic0 hashes, for a log filter:
| Event | Emitted by | When | topic0 |
|---|---|---|---|
| Graduated | The curve | Once, at graduation | 0x1c858049e704460ab9455025be4078f9e746e3fd426a56040d06389edb8197db |
| PoolCreated | The migrator | Once: the pool and what went in | 0xd569a23a8cff45c641c5d5e4fb55b5e15e918f9acf0fc42b4909adc31f5f806c |
| PositionLocked | The migrator | Once: tokenId, liquidity and the opening price | 0x4969dc20f22940c3400076fb031373f7c163d9c380f3fe04ef55156e73a9a4c7 |
| PositionLocked | The locker | Once: token and tokenId recorded | 0x2cabb2a2973327d5863ceb4707e9441851243897e86d587ee35943599752eb54 |
| Transfer | Position manager | Once, from the zero address to the locker | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef |
| Collect | Position manager | On every fee sweep, recipient = the locker | 0x40d0efd1a53d60ecbf40971b9daf7dc90178c3aadc7aab1765632738fa8b8f01 |
| FeesCollected | The locker | On every fee sweep, with both amounts | 0x0f9ec0d0f658b104ece0099070bf699e282a0981785d90a1b937678546b5caca |
| DecreaseLiquidity | Position manager | Never, for a locked position | 0x26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4 |
- Graduated
- Emitted by
- The curve
- When
- Once, at graduation
- topic0
0x1c858049e704460ab9455025be4078f9e746e3fd426a56040d06389edb8197db
- PoolCreated
- Emitted by
- The migrator
- When
- Once: the pool and what went in
- topic0
0xd569a23a8cff45c641c5d5e4fb55b5e15e918f9acf0fc42b4909adc31f5f806c
- PositionLocked
- Emitted by
- The migrator
- When
- Once: tokenId, liquidity and the opening price
- topic0
0x4969dc20f22940c3400076fb031373f7c163d9c380f3fe04ef55156e73a9a4c7
- PositionLocked
- Emitted by
- The locker
- When
- Once: token and tokenId recorded
- topic0
0x2cabb2a2973327d5863ceb4707e9441851243897e86d587ee35943599752eb54
- Transfer
- Emitted by
- Position manager
- When
- Once, from the zero address to the locker
- topic0
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
- Collect
- Emitted by
- Position manager
- When
- On every fee sweep, recipient = the locker
- topic0
0x40d0efd1a53d60ecbf40971b9daf7dc90178c3aadc7aab1765632738fa8b8f01
- FeesCollected
- Emitted by
- The locker
- When
- On every fee sweep, with both amounts
- topic0
0x0f9ec0d0f658b104ece0099070bf699e282a0981785d90a1b937678546b5caca
- DecreaseLiquidity
- Emitted by
- Position manager
- When
- Never, for a locked position
- topic0
0x26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4
Locked, time-locked or burned
| VectrLPLocker | Time-lock | Burned LP | |
|---|---|---|---|
| Can the liquidity come out? | No: no function exists that removes it | Yes, to the beneficiary once the lock expires | No: sent to an address nobody controls |
| Trading fees | Collectable by anyone, paid to Vectr's platform wallets | Depends on the lock contract; often claimable by the beneficiary | For a V3 position, never collectable |
| What you check | ownerOf is the locker, and the locker's code | The unlock date, and who can extend, withdraw or migrate | ownerOf is a burn address |
| Registry | tokenPosition and positionAt list every lock | Per lock contract | None |
- Can the liquidity come out?
- VectrLPLocker
- No: no function exists that removes it
- Time-lock
- Yes, to the beneficiary once the lock expires
- Burned LP
- No: sent to an address nobody controls
- Trading fees
- VectrLPLocker
- Collectable by anyone, paid to Vectr's platform wallets
- Time-lock
- Depends on the lock contract; often claimable by the beneficiary
- Burned LP
- For a V3 position, never collectable
- What you check
- VectrLPLocker
- ownerOf is the locker, and the locker's code
- Time-lock
- The unlock date, and who can extend, withdraw or migrate
- Burned LP
- ownerOf is a burn address
- Registry
- VectrLPLocker
- tokenPosition and positionAt list every lock
- Time-lock
- Per lock contract
- Burned LP
- None
What locked does not mean
Locked liquidity means the pool can always be traded against. It does not hold the price up: holders can sell into the locked position, and the price falls as they do. Anyone can add their own liquidity to the pool beside the locked position, and that liquidity is theirs to remove.
The lock covers positions that exist. The migrator's owner can change the fee tier for pools it creates later, and the factory owner can point future launches at a different migrator; a curve keeps the migrator it was created with, and positions already in this locker are unaffected. Every admin power is listed on the contract registry, the graduation sequence is in token graduation, the three layers of tests in the graduation reference, and the trading risks in the risk disclosure.
Locked liquidity questions
Can Vectr remove the liquidity of a graduated token?
No. The position NFT belongs to VectrLPLocker, which has no owner and no function that transfers, approves, decreases or burns a position. Uniswap's position manager only lets the owner or an approved address move or unwind a position, and the locker can never approve anyone. The contracts test suite asserts both: every transfer or decreaseLiquidity attempt reverts, and the locker's ABI contains none of those functions.
Who receives the trading fees from a locked position?
Vectr's platform wallets. Anyone can call collect(tokenId) on the locker; it pulls the fees the position has earned from Uniswap and forwards them through the FeeCollector's platform split (40% / 40% / 20% across 3 wallets today). WETH is unwrapped to ETH on the way; the token side and any Stock Token side are paid as those tokens.
Does the token's creator earn from the locked position?
No. The creator's income is the creator share of curve fees, which stops at graduation and stays claimable in the FeeCollector. The pool fee from the locked position goes to the platform wallets.
How do I find the position ID for a token?
Call tokenPosition(token) on VectrLPLocker, or tokenToPosition(token) on the migrator the token's curve was created with. Both return the same ID, which is also in the PositionLocked events of the graduation transaction.
Does locked liquidity mean the price cannot fall?
No. It means the pool always has liquidity to trade against, so sellers can always sell. It says nothing about the price: holders can sell into the locked position until the price is a small fraction of where the curve ended.
Could a future Vectr contract unlock existing positions?
Not these ones. The locker is a plain contract, not a proxy, deployed by the migrator's constructor, and a curve's migrator is fixed when the curve is created. The factory owner can point future launches at a new migrator; positions already in this locker stay where they are.