BondingCurveV3

0xc43ec73f0f7cf13f2ce8fa5c22362705958ced23

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
6,569 bytes
ABI entries
22
10 read · 4 write
License
none

Contract information

Address
0xc43ec73f0f7cf13f2ce8fa5c22362705958ced23
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0x248bdef104…2a7487f852

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (10)

FEE_BPS()uint256
V3_FEE()uint24
creator()address
factory()address
migrated()bool
migrationThreshold()uint256
realEthReserve()uint256
token()address
virtualEthReserve()uint256
virtualTokenReserve()uint256

Events (4)

BuyFeePaymentFailedMigratedSell

ABI

[
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "virtualEthReserve_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "virtualTokenReserve_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "migrationThreshold_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "MathOverflowedMulDiv",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "buyer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      }
    ],
    "name": "Buy",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "FeePaymentFailed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "positionId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAdded",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensAdded",
        "type": "uint256"
      }
    ],
    "name": "Migrated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "seller",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokensIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "name": "Sell",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "FEE_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "V3_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buy",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      }
    ],
    "name": "buyFor",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creator",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factory",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "contract LaunchToken",
        "name": "token_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "creator_",
        "type": "address"
      }
    ],
    "name": "initialize",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "migrated",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "migrationThreshold",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "realEthReserve",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "contract LaunchToken",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "virtualEthReserve",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "virtualTokenReserve",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "./LaunchToken.sol";
import "./interfaces/IUniswapV3.sol";

interface IRawbinFactoryV3View {
    function feeRecipient() external view returns (address);
    function npm() external view returns (INonfungiblePositionManager);
    function locker() external view returns (address);
}

interface IRawbinLocker {
    function lockPosition(uint256 tokenId, address creator) external;
}

/// @title BondingCurveV3
/// @custom:mission We're building a transparent launchpad. Verified contracts, no
///  hidden owner over tokens, no token tax, liquidity you can never rug. Read the
///  code and verify everything yourself. Every launch is a drop. — rawbin
/// @notice Constant-product virtual-reserve bonding curve. Identical trading to v2,
///         but at migrationThreshold it seeds a Uniswap V3 pool (1% tier) and locks
///         the position NFT in the Rawbin Locker forever — liquidity can never be
///         pulled, and trading fees keep flowing (ETH -> platform, token -> creator).
contract BondingCurveV3 is ReentrancyGuard {
    LaunchToken public token;
    address public immutable factory;
    address public creator;

    uint256 public virtualEthReserve;
    uint256 public virtualTokenReserve;
    uint256 public realEthReserve;

    uint256 public constant FEE_BPS = 100; // 1%
    uint24 public constant V3_FEE = 10000; // 1% tier
    int24 internal constant TICK_LOWER = -887200; // full range, multiple of 200 (1% spacing)
    int24 internal constant TICK_UPPER = 887200;

    uint256 public immutable migrationThreshold;
    bool public migrated;
    bool private _initialized;

    event Buy(address indexed buyer, uint256 ethIn, uint256 tokensOut);
    event Sell(address indexed seller, uint256 tokensIn, uint256 ethOut);
    event Migrated(address indexed pool, uint256 positionId, uint256 ethAdded, uint256 tokensAdded);
    event FeePaymentFailed(address indexed recipient, uint256 amount);

    constructor(
        uint256 virtualEthReserve_,
        uint256 virtualTokenReserve_,
        uint256 migrationThreshold_
    ) {
        factory = msg.sender;
        virtualEthReserve = virtualEthReserve_;
        virtualTokenReserve = virtualTokenReserve_;
        migrationThreshold = migrationThreshold_;
    }

    /// @notice Set token + creator once — factory-only, in the same tx as deploy.
    function initialize(LaunchToken token_, address creator_) external {
        require(msg.sender == factory, "only factory");
        require(!_initialized, "already initialized");
        require(creator_ != address(0), "creator=0");
        _initialized = true;
        token = token_;
        creator = creator_;
    }

    modifier onlyInitialized() {
        require(_initialized, "not initialized");
        _;
    }

    modifier notMigrated() {
        require(!migrated, "already migrated to DEX");
        _;
    }

    /// @notice Buy tokens with ETH along the constant-product curve.
    function buy(uint256 minTokensOut) external payable nonReentrant notMigrated onlyInitialized {
        _buy(msg.sender, minTokensOut);
    }

    /// @notice Buy for `recipient` — the creator's dev buy inside createToken.
    function buyFor(address recipient, uint256 minTokensOut)
        external payable nonReentrant notMigrated onlyInitialized
    {
        require(recipient != address(0), "recipient=0");
        _buy(recipient, minTokensOut);
    }

    // Guarded: the only callers are buy()/buyFor(), both `nonReentrant`, and
    // _migrate() follows checks-effects-interactions (migrated=true and
    // realEthReserve=0 are set before any external call).
    // slither-disable-next-line reentrancy-eth
    function _buy(address recipient, uint256 minTokensOut) internal {
        require(msg.value > 0, "eth=0");

        uint256 fee = (msg.value * FEE_BPS) / 10_000;
        uint256 ethIn = msg.value - fee;

        // constant product: (vEth+ethIn)*(vToken-tokensOut) = k.
        // Round newVirtualToken UP so rounding favors the curve, not the trader
        // (else dust lets traders drain more ETH than the reserve holds).
        uint256 k = virtualEthReserve * virtualTokenReserve;
        uint256 newVirtualEth = virtualEthReserve + ethIn;
        uint256 newVirtualToken = (k + newVirtualEth - 1) / newVirtualEth;
        uint256 tokensOut = virtualTokenReserve - newVirtualToken;

        require(tokensOut >= minTokensOut, "slippage");
        require(tokensOut <= token.balanceOf(address(this)), "insufficient curve liquidity");

        virtualEthReserve = newVirtualEth;
        virtualTokenReserve = newVirtualToken;
        realEthReserve += ethIn;

        _payFee(fee);

        require(token.transfer(recipient, tokensOut), "token transfer failed");
        emit Buy(recipient, msg.value, tokensOut);

        if (realEthReserve >= migrationThreshold) {
            _migrate();
        }
    }

    /// @notice Sell tokens back into the curve for ETH.
    function sell(uint256 tokenAmount, uint256 minEthOut) external nonReentrant notMigrated onlyInitialized {
        require(tokenAmount > 0, "amount=0");
        require(token.transferFrom(msg.sender, address(this), tokenAmount), "transferFrom failed");

        // newVirtualEth rounded UP (favors the curve) — see matching note in buy().
        uint256 k = virtualEthReserve * virtualTokenReserve;
        uint256 newVirtualToken = virtualTokenReserve + tokenAmount;
        uint256 newVirtualEth = (k + newVirtualToken - 1) / newVirtualToken;
        uint256 ethOutGross = virtualEthReserve - newVirtualEth;

        uint256 fee = (ethOutGross * FEE_BPS) / 10_000;
        uint256 ethOut = ethOutGross - fee;

        require(ethOut >= minEthOut, "slippage");
        // Bound against the GROSS amount — realEthReserve is reduced by ethOutGross.
        require(ethOutGross <= realEthReserve, "insufficient real reserve");

        virtualEthReserve = newVirtualEth;
        virtualTokenReserve = newVirtualToken;
        realEthReserve -= ethOutGross;

        _payFee(fee);

        (bool sent, ) = msg.sender.call{value: ethOut}("");
        require(sent, "eth transfer failed");
        emit Sell(msg.sender, tokenAmount, ethOut);
    }

    /// @dev Non-blocking fee send: reads the live platform recipient from the
    ///      factory (so a timelocked change applies to every curve at once). If the
    ///      recipient rejects the transfer, the fee stays in the curve and trading
    ///      is never halted — the failure is logged instead of reverting.
    function _payFee(uint256 fee) internal {
        if (fee == 0) return;
        address r = IRawbinFactoryV3View(factory).feeRecipient();
        (bool ok, ) = r.call{value: fee}("");
        if (!ok) emit FeePaymentFailed(r, fee);
    }

    /// @dev Seeds a Uniswap V3 pool (1% tier) with the collected ETH + remaining
    ///      tokens as a full-range position, then transfers the position NFT to the
    ///      Locker (held forever — no withdraw). Only fees are ever collectable.
    function _migrate() internal {
        migrated = true;

        INonfungiblePositionManager npm = IRawbinFactoryV3View(factory).npm();
        address locker = IRawbinFactoryV3View(factory).locker();
        address weth = npm.WETH9();

        uint256 ethForLp = realEthReserve;
        uint256 tokenForLp = token.balanceOf(address(this));
        realEthReserve = 0;

        // Wrap ETH and approve the position manager for both sides.
        IWETH9(weth).deposit{value: ethForLp}();
        require(IWETH9(weth).approve(address(npm), ethForLp), "weth approve failed");
        require(token.approve(address(npm), tokenForLp), "token approve failed");

        // Sort tokens; token0 must be the lower address (Uniswap invariant).
        (address token0, address token1, uint256 amount0, uint256 amount1) =
            address(token) < weth
                ? (address(token), weth, tokenForLp, ethForLp)
                : (weth, address(token), ethForLp, tokenForLp);

        uint160 sqrtPriceX96 = _sqrtPriceX96(amount0, amount1);

        address pool = npm.createAndInitializePoolIfNecessary(token0, token1, V3_FEE, sqrtPriceX96);

        (uint256 positionId, , , ) = npm.mint(
            INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: V3_FEE,
                tickLower: TICK_LOWER,
                tickUpper: TICK_UPPER,
                amount0Desired: amount0,
                amount1Desired: amount1,
                amount0Min: 0,
                amount1Min: 0,
                recipient: locker, // NFT goes straight to the locker
                deadline: block.timestamp
            })
        );

        IRawbinLocker(locker).lockPosition(positionId, creator);

        emit Migrated(pool, positionId, ethForLp, tokenForLp);
    }

    /// @dev sqrtPriceX96 = sqrt(amount1/amount0) * 2^96, computed overflow-safe:
    ///      r = (amount1 << 96) / amount0 = price * 2^96;
    ///      sqrt(r) = sqrt(price) * 2^48  =>  sqrtPriceX96 = sqrt(r) << 48.
    function _sqrtPriceX96(uint256 amount0, uint256 amount1) internal pure returns (uint160) {
        require(amount0 > 0 && amount1 > 0, "amount=0");
        uint256 r = Math.mulDiv(amount1, 1 << 96, amount0);
        uint256 sqrtP = Math.sqrt(r) << 48;
        require(sqrtP <= type(uint160).max, "sqrtPrice overflow");
        // Safe: bounded by the require above.
        // forge-lint: disable-next-line(unsafe-typecast)
        return uint160(sqrtP);
    }

    receive() external payable {}
}
Chain explorer5725msChain node102ms