Learn

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

Definition

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.

  1. 01
    Curve
    _graduate()

    Hands the raise and the remaining tokens to the migrator it was created with.

  2. 02
    Migrator
    migrate()

    Creates the Uniswap V3 pool and prices it where the curve ended.

  3. 03
    Position manager
    mint(...)

    Mints one full-range position NFT with the locker as recipient.

  4. 04
    VectrLPLocker
    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
Surface

Every function the locker has

The whole external interface of VectrLPLocker. Nothing else can be called on it.

Absent by design

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.

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

Fees

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
Verify

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.

  1. 1
    Find the curve
    Ask the factory for the token's curve and confirm it has graduated.
    factory.getTokenCurve(token) -> curve; curve.graduated() -> true
  2. 2
    Find its migrator and position
    A 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
  3. 3
    Ask the locker
    The locker should name the same migrator and the same position.
    locker.migrator() -> migrator; locker.tokenPosition(token) -> tokenId
  4. 4
    Ask Uniswap who owns it
    The 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
  5. 5
    Check the range and the liquidity
    positions(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
  6. 6
    Read the history
    The 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)" $ID

In 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:

  • 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
A DecreaseLiquidity event for a locked tokenId, or a Transfer of it away from the locker, would mean the lock failed. The contracts cannot produce either; the check exists so you do not have to take that on faith. Contract source is publicly verified on the linked explorer. Source verification is not an independent security audit.
Compared

Locked, time-locked or burned

Limits

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.

FAQ

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.