Neura DocsNeuraV1Pool

Smart contracts

NeuraV1Pool

Full source of the hybrid pool.

The core contract. Key functions: mint, burn, swap, getAmountOut, currentFee, skim, sync.

solidity
// SPDX-License-Identifier: BUSL-1.1pragma solidity 0.8.28;
import {IERC20Minimal, INeuraV1Callee, INeuraV1Factory} from "./interfaces/INeuraV1.sol";import {NeuraMath} from "./libraries/NeuraMath.sol";
/// @title Neura V1 Hybrid Pool/// @notice One pool contract, two curves:///         - Volatile: x * y >= k               (any token pair)///         - Stable:   x^3*y + x*y^3 >= k       (correlated pairs, e.g. USDC/USDT)///         plus a fee tier chosen at creation and a same-block "surge" fee that///         charges later swaps in a block; this is not guaranteed MEV protection./// @dev    UNAUDITED. Not deployed. Reference implementation for the Neura docs.contract NeuraV1Pool {    // ---------------------------------------------------------------- LP token    string public constant name = "Neura V1 LP";    string public constant symbol = "NEURA-V1-LP";    uint8 public constant decimals = 18;    uint256 public totalSupply;    mapping(address => uint256) public balanceOf;    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);    event Approval(address indexed owner, address indexed spender, uint256 value);
    // ---------------------------------------------------------------- config    uint256 public constant MINIMUM_LIQUIDITY = 1_000;    uint256 public constant FEE_DENOMINATOR = 1_000_000; // fees in pips (1e-6)    uint256 public constant MAX_TOTAL_FEE = 10_000;      // 1.00% hard cap    uint256 public constant MAX_NORMALIZED_RESERVE = 1e28;    uint256 public constant MIN_STABLE_NORMALIZED_RESERVE = 1e15;
    address public immutable factory;    address public immutable token0;    address public immutable token1;    bool public immutable stable;    uint24 public immutable baseFee;   // e.g. 100 = 0.01%, 3000 = 0.30%    uint24 public immutable surgeFee;  // extra fee for 2nd+ swap in the same block    uint256 internal immutable decimals0;    uint256 internal immutable decimals1;
    // ---------------------------------------------------------------- state    uint112 public reserve0;    uint112 public reserve1;    uint32 public blockTimestampLast;    uint256 public price0CumulativeLast; // TWAP oracle (UQ112x112 style, scaled 1e18)    uint256 public price1CumulativeLast;    uint256 public lastSwapBlock;    uint256 private unlocked = 1;
    event Mint(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);    event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);    event Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, uint256 feePips, address indexed to);    event Sync(uint112 reserve0, uint112 reserve1);
    error Locked();    error InsufficientLiquidityMinted();    error InsufficientLiquidityBurned();    error InsufficientOutput();    error InsufficientInput();    error InsufficientLiquidity();    error InvalidTo();    error KInvariant();    error Overflow();    error TransferFailed();    error UnsupportedDecimals();
    modifier lock() {        if (unlocked != 1) revert Locked();        unlocked = 2;        _;        unlocked = 1;    }
    constructor() {        (address t0, address t1, bool s, uint24 f, uint24 surge) = INeuraV1Factory(msg.sender).parameters();        require(f + surge <= MAX_TOTAL_FEE, "fee");        factory = msg.sender;        (token0, token1, stable, baseFee, surgeFee) = (t0, t1, s, f, surge);        uint8 d0 = IERC20Minimal(t0).decimals();        uint8 d1 = IERC20Minimal(t1).decimals();        if (d0 > 18 || d1 > 18) revert UnsupportedDecimals();        decimals0 = 10 ** d0;        decimals1 = 10 ** d1;    }
    // ---------------------------------------------------------------- views    function getReserves() public view returns (uint112, uint112, uint32) {        return (reserve0, reserve1, blockTimestampLast);    }
    /// @notice Fee (in pips) the next swap in this block will pay.    function currentFee() public view returns (uint256) {        return lastSwapBlock == block.number ? uint256(baseFee) + surgeFee : baseFee;    }
    /// @notice Quote for routers / the Neura interface.    function getAmountOut(uint256 amountIn, address tokenIn) external view returns (uint256) {        require(tokenIn == token0 || tokenIn == token1, "token");        (uint256 r0, uint256 r1) = (reserve0, reserve1);        if (amountIn == 0 || r0 == 0 || r1 == 0) return 0;        if (amountIn > type(uint112).max) revert Overflow();        amountIn -= (amountIn * currentFee()) / FEE_DENOMINATOR;        return _amountOut(amountIn, tokenIn, r0, r1);    }
    // ---------------------------------------------------------------- liquidity    function mint(address to) external lock returns (uint256 liquidity) {        if (to == address(0) || to == address(this)) revert InvalidTo();        (uint256 r0, uint256 r1) = (reserve0, reserve1);        uint256 b0 = IERC20Minimal(token0).balanceOf(address(this));        uint256 b1 = IERC20Minimal(token1).balanceOf(address(this));        _checkBalances(b0, b1);        uint256 a0 = b0 - r0;        uint256 a1 = b1 - r1;        uint256 supply = totalSupply;        if (supply == 0) {            uint256 initial = NeuraMath.sqrt(a0 * a1);            uint256 lockedLiquidity = MINIMUM_LIQUIDITY;            if (stable) {                // first deposit must be balanced in normalized units, otherwise the                // stable invariant can be skewed by a dust first depositor.                require((a0 * 1e18) / decimals0 == (a1 * 1e18) / decimals1, "stable: unbalanced init");                require(_k(a0, a1) > MINIMUM_LIQUIDITY, "stable: k");                // Keep the locked tail above the integer stable-invariant dust domain.                lockedLiquidity = NeuraMath.sqrt(decimals0 * decimals1) * MIN_STABLE_NORMALIZED_RESERVE / 1e18;                if (lockedLiquidity < MINIMUM_LIQUIDITY) lockedLiquidity = MINIMUM_LIQUIDITY;            }            if (initial <= lockedLiquidity) revert InsufficientLiquidityMinted();            liquidity = initial - lockedLiquidity;            _mint(address(0xdead), lockedLiquidity);        } else {            liquidity = NeuraMath.min((a0 * supply) / r0, (a1 * supply) / r1);        }        if (liquidity == 0) revert InsufficientLiquidityMinted();        _mint(to, liquidity);        _update(b0, b1, r0, r1);        emit Mint(msg.sender, a0, a1, to);    }
    function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {        if (to == address(0) || to == address(this) || to == token0 || to == token1) revert InvalidTo();        (uint256 r0, uint256 r1) = (reserve0, reserve1);        uint256 b0 = IERC20Minimal(token0).balanceOf(address(this));        uint256 b1 = IERC20Minimal(token1).balanceOf(address(this));        uint256 liquidity = balanceOf[address(this)];        uint256 supply = totalSupply;        amount0 = (liquidity * b0) / supply;        amount1 = (liquidity * b1) / supply;        if (amount0 == 0 || amount1 == 0) revert InsufficientLiquidityBurned();        _burn(address(this), liquidity);        _safeTransfer(token0, to, amount0);        _safeTransfer(token1, to, amount1);        b0 = IERC20Minimal(token0).balanceOf(address(this));        b1 = IERC20Minimal(token1).balanceOf(address(this));        _update(b0, b1, r0, r1);        emit Burn(msg.sender, amount0, amount1, to);    }
    // ---------------------------------------------------------------- swap    /// @notice Low-level swap. Optimistically sends outputs, optionally calls    ///         back `to` (flash swap), then enforces the invariant after fees.    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external lock {        if (amount0Out == 0 && amount1Out == 0) revert InsufficientOutput();        (uint256 r0, uint256 r1) = (reserve0, reserve1);        if (amount0Out >= r0 || amount1Out >= r1) revert InsufficientLiquidity();        if (to == address(0) || to == address(this) || to == token0 || to == token1) revert InvalidTo();
        uint256 feePips = currentFee();        if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);        if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);        if (data.length > 0) INeuraV1Callee(to).neuraV1Call(msg.sender, amount0Out, amount1Out, data);
        uint256 b0 = IERC20Minimal(token0).balanceOf(address(this));        uint256 b1 = IERC20Minimal(token1).balanceOf(address(this));        uint256 in0 = b0 > r0 - amount0Out ? b0 - (r0 - amount0Out) : 0;        uint256 in1 = b1 > r1 - amount1Out ? b1 - (r1 - amount1Out) : 0;        _checkBalances(b0, b1);        if (in0 == 0 && in1 == 0) revert InsufficientInput();
        // invariant is checked on balances minus the fee taken from the input side        uint256 adj0 = b0 - (in0 * feePips) / FEE_DENOMINATOR;        uint256 adj1 = b1 - (in1 * feePips) / FEE_DENOMINATOR;        if (_k(adj0, adj1) < _k(r0, r1)) revert KInvariant();
        lastSwapBlock = block.number;        _update(b0, b1, r0, r1);        emit Swap(msg.sender, in0, in1, amount0Out, amount1Out, feePips, to);    }
    /// @notice Send donated / excess tokens out so balances match reserves.    function skim(address to) external lock {        _safeTransfer(token0, to, IERC20Minimal(token0).balanceOf(address(this)) - reserve0);        _safeTransfer(token1, to, IERC20Minimal(token1).balanceOf(address(this)) - reserve1);    }
    /// @notice Force reserves to match balances.    function sync() external lock {        _update(IERC20Minimal(token0).balanceOf(address(this)), IERC20Minimal(token1).balanceOf(address(this)), reserve0, reserve1);    }
    // ---------------------------------------------------------------- math    function _k(uint256 x, uint256 y) internal view returns (uint256) {        if (!stable) return x * y;        uint256 _x = (x * 1e18) / decimals0;        uint256 _y = (y * 1e18) / decimals1;        return NeuraMath.stableK(_x, _y);    }
    function _amountOut(uint256 amountIn, address tokenIn, uint256 r0, uint256 r1) internal view returns (uint256) {        bool zeroForOne = tokenIn == token0;        require(zeroForOne || tokenIn == token1, "token");        if (!stable) {            (uint256 vIn, uint256 vOut) = zeroForOne ? (r0, r1) : (r1, r0);            return (amountIn * vOut) / (vIn + amountIn);        }        uint256 xy = _k(r0, r1);        uint256 n0 = (r0 * 1e18) / decimals0;        uint256 n1 = (r1 * 1e18) / decimals1;        (uint256 rIn, uint256 rOut, uint256 dIn, uint256 dOut) = zeroForOne ? (n0, n1, decimals0, decimals1) : (n1, n0, decimals1, decimals0);        uint256 x = rIn + (amountIn * 1e18) / dIn;        if (x > MAX_NORMALIZED_RESERVE) revert Overflow();        uint256 y = rOut - NeuraMath.stableGetY(x, xy, rOut);        return (y * dOut) / 1e18;    }
    function _update(uint256 b0, uint256 b1, uint256 r0, uint256 r1) private {        _checkBalances(b0, b1);        if (stable && totalSupply > 0 && _k(b0, b1) <= MINIMUM_LIQUIDITY) revert KInvariant();        uint32 ts = uint32(block.timestamp);        unchecked {            uint32 elapsed = ts - blockTimestampLast;            // Stable reserve ratios are not the curve's marginal prices. Do not            // expose them as an oracle: stable cumulative fields remain zero.            if (!stable && elapsed > 0 && r0 != 0 && r1 != 0) {                price0CumulativeLast += ((r1 * 1e18) / r0) * elapsed;                price1CumulativeLast += ((r0 * 1e18) / r1) * elapsed;            }        }        reserve0 = uint112(b0);        reserve1 = uint112(b1);        blockTimestampLast = ts;        emit Sync(reserve0, reserve1);    }
    function _checkBalances(uint256 b0, uint256 b1) private view {        if (b0 > type(uint112).max || b1 > type(uint112).max) revert Overflow();        if (stable && ((b0 * 1e18) / decimals0 > MAX_NORMALIZED_RESERVE || (b1 * 1e18) / decimals1 > MAX_NORMALIZED_RESERVE)) revert Overflow();    }
    // ---------------------------------------------------------------- ERC20    function approve(address spender, uint256 value) external returns (bool) {        allowance[msg.sender][spender] = value;        emit Approval(msg.sender, spender, value);        return true;    }
    function transfer(address to, uint256 value) external returns (bool) {        _transfer(msg.sender, to, value);        return true;    }
    function transferFrom(address from, address to, uint256 value) external returns (bool) {        uint256 a = allowance[from][msg.sender];        if (a != type(uint256).max) allowance[from][msg.sender] = a - value;        _transfer(from, to, value);        return true;    }
    function _transfer(address from, address to, uint256 value) private {        if (to == address(0) || from == address(0xdead)) revert InvalidTo();        balanceOf[from] -= value;        balanceOf[to] += value;        emit Transfer(from, to, value);    }
    function _mint(address to, uint256 value) private {        totalSupply += value;        balanceOf[to] += value;        emit Transfer(address(0), to, value);    }
    function _burn(address from, uint256 value) private {        balanceOf[from] -= value;        totalSupply -= value;        emit Transfer(from, address(0), value);    }
    function _safeTransfer(address token, address to, uint256 value) private {        (bool ok, bytes memory ret) = token.call(abi.encodeWithSelector(IERC20Minimal.transfer.selector, to, value));        if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert TransferFailed();    }}