LpVault

0xade0f1966c2E0dD058CFB7e68da60767433b74DBVerified
Native balance0USDX
Account typeSmart contract
NetworkTuven 99359

Overview

Contract name
LpVault
Verification
Verified source
Balance updated at block
10874

Verified contract source

Verified
Contract name
LpVault
Compiler
v0.8.29+commit.ab55807c
EVM version
prague
License
none
// 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
JSON
[
  {
    "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"
  }
]

YOUR GATEWAY TO TUVEN

Connect to possibility.

Choose a wallet. Your assets stay in your control.

No wallet detected. On mobile, open this page in your wallet’s built-in browser.
Get MetaMask

Tuven · Chain 99359 · USDX