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
- Creator
- 0x48fb55ee52…5A687e0999
- 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