LpVault
0xade0f1966c2E0dD058CFB7e68da60767433b74DBVerifiedNative balance0USDX
Account typeSmart contract
NetworkTuven 99359
Overview
- Contract name
- LpVault
- Verification
- Verified source
- Creation transaction
- 0x13a49c2f1179dff22aa4d272b7ea2a5194937943c06ab6cd0b08a771db1a0a07
- Balance updated at block
- 10874
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
import {IUniswapV2Pair} from "./interfaces/IUniswapV2.sol";
interface ILaunchpadAdmin {
function feeRecipient() external view returns (address);
function owner() external view returns (address);
}
/// @title LpVault
/// @notice Platform custody for the Uniswap V2 LP tokens minted at graduation.
///
/// The principal liquidity is locked: no function in this version can
/// withdraw it. The vault harvests only the trading fees the position
/// earns. In a constant-product pool, the liquidity units behind one LP
/// token (`sqrt(k) / totalSupply`) only grow through swap fees (price
/// moves and proportional mints/burns leave it unchanged). So the number
/// of LP tokens that still represents the original deposit shrinks over
/// time, and the surplus LP is pure fee growth. `harvest` burns exactly
/// that surplus and sends the underlying tokens to the launchpad's fee
/// recipient. Impermanent loss never leaks into the harvest because the
/// accounting is in liquidity units, not token amounts.
///
/// Upgradeability: UUPS proxy, deployed by the Launchpad in its
/// constructor. Upgrades are authorised by the launchpad's owner (the
/// admin multisig), so the lock is as strong as that owner's governance.
/// Storage is ERC-7201 namespaced so upgrades cannot collide with it.
contract LpVault is Initializable, UUPSUpgradeable {
struct Position {
address token; // the LaunchToken paired with USDX
uint256 principalLp; // LP received at graduation
uint256 sqrtK0; // sqrt(reserve0 * reserve1) right after the deposit
uint256 supply0; // pair totalSupply right after the deposit
}
/// @custom:storage-location erc7201:tuven.launchpad.LpVault
struct VaultStorage {
address launchpad;
mapping(address pair => Position) positions;
address[] allPairs;
}
// keccak256(abi.encode(uint256(keccak256("tuven.launchpad.LpVault")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant VAULT_STORAGE = 0x0874d0cc472e29a89e6fed94e9a59c1918090da66dac9c71895b92a1b0e7ed00;
event Registered(address indexed pair, address indexed token, uint256 principalLp, uint256 sqrtK0, uint256 supply0);
event Harvested(address indexed pair, address indexed to, uint256 lpBurned, uint256 amount0, uint256 amount1);
error OnlyLaunchpad();
error NotAuthorized();
error AlreadyRegistered();
error UnknownPair();
error LpNotReceived();
error NothingToHarvest();
error ZeroAddress();
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address launchpad_) external initializer {
if (launchpad_ == address(0)) revert ZeroAddress();
_s().launchpad = launchpad_;
}
// ── Launchpad hooks ──────────────────────────────────────────────────
/// @notice Records a graduation deposit. The LP must already be in the vault.
function register(address pair, address token, uint256 lpAmount) external {
VaultStorage storage s = _s();
if (msg.sender != s.launchpad) revert OnlyLaunchpad();
if (s.positions[pair].principalLp != 0) revert AlreadyRegistered();
if (lpAmount == 0 || IERC20(pair).balanceOf(address(this)) < lpAmount) revert LpNotReceived();
(uint112 r0, uint112 r1,) = IUniswapV2Pair(pair).getReserves();
uint256 sqrtK0 = Math.sqrt(uint256(r0) * uint256(r1));
uint256 supply0 = IUniswapV2Pair(pair).totalSupply();
s.positions[pair] = Position({token: token, principalLp: lpAmount, sqrtK0: sqrtK0, supply0: supply0});
s.allPairs.push(pair);
emit Registered(pair, token, lpAmount, sqrtK0, supply0);
}
// ── Fee harvest ──────────────────────────────────────────────────────
/// @notice Burns the fee-growth surplus LP of `pair` and sends the underlying
/// tokens to the launchpad's fee recipient. Never touches the principal.
function harvest(address pair) external returns (uint256 lpBurned, uint256 amount0, uint256 amount1) {
VaultStorage storage s = _s();
if (s.positions[pair].principalLp == 0) revert UnknownPair();
lpBurned = harvestable(pair);
if (lpBurned == 0) revert NothingToHarvest();
address to = ILaunchpadAdmin(s.launchpad).feeRecipient();
IERC20(pair).transfer(pair, lpBurned);
(amount0, amount1) = IUniswapV2Pair(pair).burn(to);
emit Harvested(pair, to, lpBurned, amount0, amount1);
}
// ── Views ────────────────────────────────────────────────────────────
function launchpad() external view returns (address) {
return _s().launchpad;
}
function positions(address pair) external view returns (Position memory) {
return _s().positions[pair];
}
function allPairs(uint256 i) external view returns (address) {
return _s().allPairs[i];
}
function allPairsLength() external view returns (uint256) {
return _s().allPairs.length;
}
/// @notice LP tokens that today represent the same liquidity units as the
/// original deposit. Rounded up so rounding can never eat principal.
function principalLp(address pair) public view returns (uint256) {
Position storage p = _s().positions[pair];
if (p.principalLp == 0) return 0;
(uint112 r0, uint112 r1,) = IUniswapV2Pair(pair).getReserves();
uint256 sqrtK = Math.sqrt(uint256(r0) * uint256(r1));
uint256 supply = IUniswapV2Pair(pair).totalSupply();
if (sqrtK == 0 || supply == 0) return p.principalLp;
return Math.mulDiv(p.principalLp * p.sqrtK0, supply, p.supply0 * sqrtK, Math.Rounding.Ceil);
}
/// @notice LP tokens that can be burned right now without touching principal.
function harvestable(address pair) public view returns (uint256) {
uint256 held = IERC20(pair).balanceOf(address(this));
uint256 principal = principalLp(pair);
return held > principal ? held - principal : 0;
}
// ── Upgrades ─────────────────────────────────────────────────────────
/// @dev Only the launchpad's owner (admin multisig) may upgrade.
function _authorizeUpgrade(address) internal view override {
if (msg.sender != ILaunchpadAdmin(_s().launchpad).owner()) revert NotAuthorized();
}
function _s() private pure returns (VaultStorage storage $) {
assembly {
$.slot := VAULT_STORAGE
}
}
}
Contract ABI
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "target",
"type": "address"
}
],
"name": "AddressEmptyCode",
"type": "error"
},
{
"inputs": [],
"name": "AlreadyRegistered",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "ERC1967InvalidImplementation",
"type": "error"
},
{
"inputs": [],
"name": "ERC1967NonPayable",
"type": "error"
},
{
"inputs": [],
"name": "FailedCall",
"type": "error"
},
{
"inputs": [],
"name": "InvalidInitialization",
"type": "error"
},
{
"inputs": [],
"name": "LpNotReceived",
"type": "error"
},
{
"inputs": [],
"name": "NotAuthorized",
"type": "error"
},
{
"inputs": [],
"name": "NotInitializing",
"type": "error"
},
{
"inputs": [],
"name": "NothingToHarvest",
"type": "error"
},
{
"inputs": [],
"name": "OnlyLaunchpad",
"type": "error"
},
{
"inputs": [],
"name": "UUPSUnauthorizedCallContext",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "slot",
"type": "bytes32"
}
],
"name": "UUPSUnsupportedProxiableUUID",
"type": "error"
},
{
"inputs": [],
"name": "UnknownPair",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "lpBurned",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
}
],
"name": "Harvested",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint64",
"name": "version",
"type": "uint64"
}
],
"name": "Initialized",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "token",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "principalLp",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "sqrtK0",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "supply0",
"type": "uint256"
}
],
"name": "Registered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "implementation",
"type": "address"
}
],
"name": "Upgraded",
"type": "event"
},
{
"inputs": [],
"name": "UPGRADE_INTERFACE_VERSION",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "i",
"type": "uint256"
}
],
"name": "allPairs",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "allPairsLength",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"name": "harvest",
"outputs": [
{
"internalType": "uint256",
"name": "lpBurned",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"name": "harvestable",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "launchpad_",
"type": "address"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "launchpad",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"name": "positions",
"outputs": [
{
"components": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "principalLp",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "sqrtK0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "supply0",
"type": "uint256"
}
],
"internalType": "struct LpVault.Position",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pair",
"type": "address"
}
],
"name": "principalLp",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "proxiableUUID",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pair",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "lpAmount",
"type": "uint256"
}
],
"name": "register",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newImplementation",
"type": "address"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "upgradeToAndCall",
"outputs": [],
"stateMutability": "payable",
"type": "function"
}
]