// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "./StockOracle.sol"; /// @notice The coin itself: a plain ERC-20, whole supply minted once to its /// curve, no owner, no mint, no pause, no transfer hook. There is /// nothing in here for anyone to abuse later. contract PairedToken { string public name; string public symbol; uint8 public constant decimals = 18; uint256 public totalSupply; string public metadataURI; address public immutable curve; 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); error BadRecipient(); error Balance(); error Allowance(); constructor(string memory n, string memory s, string memory uri, uint256 supply) { name = n; symbol = s; metadataURI = uri; curve = msg.sender; totalSupply = supply; balanceOf[msg.sender] = supply; emit Transfer(address(0), msg.sender, supply); } function transfer(address to, uint256 v) external returns (bool) { _move(msg.sender, to, v); return true; } function approve(address sp, uint256 v) external returns (bool) { allowance[msg.sender][sp] = v; emit Approval(msg.sender, sp, v); return true; } function transferFrom(address f, address to, uint256 v) external returns (bool) { uint256 a = allowance[f][msg.sender]; if (a != type(uint256).max) { if (a < v) revert Allowance(); allowance[f][msg.sender] = a - v; emit Approval(f, msg.sender, a - v); } _move(f, to, v); return true; } function _move(address f, address to, uint256 v) internal { if (to == address(0) || to == address(this)) revert BadRecipient(); uint256 b = balanceOf[f]; if (b < v) revert Balance(); unchecked { balanceOf[f] = b - v; balanceOf[to] += v; } emit Transfer(f, to, v); } } /// @title StockPairedCurve /// @notice One bonding curve per pair, deployed by StockPairedLaunchpad. /// /// WHAT THE CURVE HOLDS. Every buy and sell settles in the chain's /// native coin, and the curve's reserve is the coin it is holding. /// Both sides of a trade are priced from that reserve and nothing /// else, so the contract can never owe more coin than it has: the /// most a seller can ever take out is what buyers put in. This is the /// whole reason the stock is not the settlement asset. A curve that /// held coin but promised payouts denominated in a share price would /// be handing out a free option on the coin/share rate, and the first /// person to notice would empty it. /// /// WHERE THE STOCK COMES IN. The pair's graduation bar is fixed at /// launch as a number of shares, not as an amount of coin, and the /// curve graduates when the coin it holds is worth that many shares /// at the oracle's current price. So if the stock doubles after /// launch, the pair needs twice the coin to fill; if it halves, the /// bar arrives sooner. That is a real link between the coin and the /// company, and it is the only place the oracle touches state. /// /// WHEN THE ORACLE IS QUIET. The graduation check is skipped while /// the feed is stale, never reverted, so a keeper that stops posting /// cannot halt a market or strand anyone's position. The curve also /// graduates unconditionally once it has sold its 800M tokens, which /// needs no oracle at all. /// /// THE MATH, identical to lib/curve.js in the pad, in wei: /// vT0 = 1,073,000,000e18 /// vQ0 = (vT0 - 800,000,000e18) * targetWei / 800,000,000e18 /// k = vT0 * vQ0 /// price = (vQ0 + raised) / (vT0 - sold) /// buy q -> out = vT - k / (vQ + q) /// sell t -> out = vQ - k / (vT + t) contract StockPairedCurve { uint256 public constant SUPPLY = 1_000_000_000e18; uint256 public constant CURVE_SUPPLY = 800_000_000e18; uint256 public constant VT0 = 1_073_000_000e18; uint256 public constant BPS = 10_000; uint256 public constant PAD_FEE_BPS = 100; // 1% of every trade uint256 public constant CREATOR_FEE_BPS = 200; // 2% of every trade uint256 public constant POOL_FEE_BPS = 100; // 1%, stays in the pool StockOracle public immutable oracle; PairedToken public immutable token; address public immutable launchpad; address public immutable treasury; address public immutable creator; bytes32 public immutable ticker; // the listed company, e.g. "META" bytes32 public immutable app; // the app on the card, e.g. "Instagram" uint256 public immutable targetShares; // graduation bar, 18 decimals of a share uint256 public immutable targetWei; // what that was worth in coin at launch uint256 public immutable sharePriceUsdE8AtLaunch; uint256 public immutable vQ0; uint256 public immutable k; uint256 public immutable createdAt; uint256 public raised; // coin held against the curve uint256 public tokensSold; uint256 public poolEth; // set at graduation, never withdrawable uint256 public poolTokens; uint256 public tradeCount; bool public graduated; event Buy(address indexed who, uint256 coinIn, uint256 tokensOut, uint256 padFee, uint256 creatorFee, uint256 refund, bool pool); event Sell(address indexed who, uint256 tokensIn, uint256 coinOut, uint256 padFee, uint256 creatorFee, bool pool); event Graduated(uint256 poolEth, uint256 poolTokens, uint256 tokensSold, uint256 at); error Expired(); error Slippage(); error ZeroAmount(); error CurveFull(); error TooManyTokens(); error NotLaunchpad(); error Reentrant(); error PayFailed(); error NoBareTransfers(); uint256 private locked = 1; modifier nonReentrant() { if (locked != 1) revert Reentrant(); locked = 2; _; locked = 1; } constructor( StockOracle _oracle, address _treasury, address _creator, bytes32 _ticker, bytes32 _app, uint256 _targetShares, uint256 _targetWei, uint256 _sharePriceUsdE8, string memory _name, string memory _symbol, string memory _uri ) { oracle = _oracle; launchpad = msg.sender; treasury = _treasury; creator = _creator; ticker = _ticker; app = _app; targetShares = _targetShares; targetWei = _targetWei; sharePriceUsdE8AtLaunch = _sharePriceUsdE8; createdAt = block.timestamp; vQ0 = ((VT0 - CURVE_SUPPLY) * _targetWei) / CURVE_SUPPLY; k = VT0 * vQ0; token = new PairedToken(_name, _symbol, _uri, SUPPLY); } // ---- views function _vQ() internal view returns (uint256) { return vQ0 + raised; } function _vT() internal view returns (uint256) { return VT0 - tokensSold; } /// @notice The price of one whole token, in wei. function priceInWei() public view returns (uint256) { if (graduated) return poolTokens == 0 ? 0 : (poolEth * 1e18) / poolTokens; return (_vQ() * 1e18) / _vT(); } /// @notice The price of one whole token in shares of `ticker`, 18 /// decimals, and whether the oracle was fresh enough to say. A /// display figure: no trade depends on it. function priceInShares() external view returns (uint256 price, bool ok) { (uint256 spe, bool fresh) = oracle.trySharesPerEth(ticker); if (!fresh) return (0, false); return ((priceInWei() * spe) / 1e18, true); } /// @notice How many shares the coin on this curve is currently worth. function sharesRaised() public view returns (uint256 shares, bool ok) { (uint256 spe, bool fresh) = oracle.trySharesPerEth(ticker); if (!fresh) return (0, false); return ((raised * spe) / 1e18, true); } function progressBps() external view returns (uint256) { return graduated ? BPS : (tokensSold * BPS) / CURVE_SUPPLY; } /// @notice Everything the interface needs, in one call. function state() external view returns ( uint256 _raised, uint256 _tokensSold, bool _graduated, uint256 _poolEth, uint256 _poolTokens, uint256 _priceWei, uint256 _targetShares, uint256 _targetWei, uint256 _tradeCount, address _token ) { return (raised, tokensSold, graduated, poolEth, poolTokens, priceInWei(), targetShares, targetWei, tradeCount, address(token)); } // ---- quotes. Pure functions of state; the interface calls these before // asking anyone to sign, and the trade functions call the same code, so // what you are quoted is what executes. function quoteBuy(uint256 value) public view returns (uint256 tokensOut, uint256 padFee, uint256 creatorFee, uint256 refund) { if (value == 0) revert ZeroAmount(); if (graduated) { padFee = (value * PAD_FEE_BPS) / BPS; creatorFee = (value * CREATOR_FEE_BPS) / BPS; uint256 poolFee = (value * POOL_FEE_BPS) / BPS; uint256 netP = value - padFee - creatorFee - poolFee; tokensOut = poolTokens - (poolEth * poolTokens) / (poolEth + netP); return (tokensOut, padFee, creatorFee, 0); } uint256 remaining = CURVE_SUPPLY - tokensSold; if (remaining == 0) revert CurveFull(); uint256 gross = value; padFee = (gross * PAD_FEE_BPS) / BPS; creatorFee = (gross * CREATOR_FEE_BPS) / BPS; uint256 net = gross - padFee - creatorFee; uint256 vQ = _vQ(); uint256 vT = _vT(); tokensOut = vT - k / (vQ + net); if (tokensOut > remaining) { // This buy would run past the end of the curve. Take only what the // last tokens cost and hand the rest straight back, so nobody pays // for supply that does not exist. uint256 needNet = k / (VT0 - CURVE_SUPPLY) - vQ; gross = (needNet * BPS) / (BPS - PAD_FEE_BPS - CREATOR_FEE_BPS) + 1; if (gross > value) gross = value; padFee = (gross * PAD_FEE_BPS) / BPS; creatorFee = (gross * CREATOR_FEE_BPS) / BPS; net = gross - padFee - creatorFee; tokensOut = vT - k / (vQ + net); if (tokensOut > remaining) tokensOut = remaining; refund = value - gross; } } function quoteSell(uint256 tokensIn) public view returns (uint256 coinOut, uint256 padFee, uint256 creatorFee) { if (tokensIn == 0) revert ZeroAmount(); uint256 gross; if (graduated) { uint256 raw = poolEth - (poolEth * poolTokens) / (poolTokens + tokensIn); gross = raw - (raw * POOL_FEE_BPS) / BPS; } else { if (tokensIn > tokensSold) revert TooManyTokens(); gross = _vQ() - k / (_vT() + tokensIn); if (gross > raised) gross = raised; } padFee = (gross * PAD_FEE_BPS) / BPS; creatorFee = (gross * CREATOR_FEE_BPS) / BPS; coinOut = gross - padFee - creatorFee; } // ---- trades function buy(uint256 minTokensOut, uint256 deadline) external payable nonReentrant returns (uint256) { return _buy(msg.sender, minTokensOut, deadline); } /// @notice The creator's first buy, ridden inside launch() so there is no /// second signature and no window where the curve sits empty. function buyFor(address to, uint256 minTokensOut, uint256 deadline) external payable nonReentrant returns (uint256) { if (msg.sender != launchpad) revert NotLaunchpad(); return _buy(to, minTokensOut, deadline); } function _buy(address to, uint256 minTokensOut, uint256 deadline) internal returns (uint256 tokensOut) { if (block.timestamp > deadline) revert Expired(); (uint256 out, uint256 padFee, uint256 creatorFee, uint256 refund) = quoteBuy(msg.value); if (out == 0 || out < minTokensOut) revert Slippage(); uint256 used = msg.value - refund; uint256 kept = used - padFee - creatorFee; // after graduation this still carries the pool's own 1% bool onPool = graduated; if (onPool) { poolEth += kept; poolTokens -= out; } else { raised += kept; tokensSold += out; } tradeCount++; emit Buy(to, used, out, padFee, creatorFee, refund, onPool); if (!onPool) _graduateIfDue(); _pay(treasury, padFee + creatorFee); if (refund > 0) _pay(msg.sender, refund); token.transfer(to, out); return out; } function sell(uint256 tokensIn, uint256 minCoinOut, uint256 deadline) external nonReentrant returns (uint256) { if (block.timestamp > deadline) revert Expired(); (uint256 out, uint256 padFee, uint256 creatorFee) = quoteSell(tokensIn); if (out < minCoinOut) revert Slippage(); uint256 gross = out + padFee + creatorFee; bool onPool = graduated; if (onPool) { poolTokens += tokensIn; poolEth -= gross; } else { raised -= gross; tokensSold -= tokensIn; } tradeCount++; emit Sell(msg.sender, tokensIn, out, padFee, creatorFee, onPool); token.transferFrom(msg.sender, address(this), tokensIn); _pay(treasury, padFee + creatorFee); _pay(msg.sender, out); return out; } // ---- graduation function _graduateIfDue() internal { if (tokensSold >= CURVE_SUPPLY) return _graduate(); (uint256 shares, bool ok) = sharesRaised(); if (ok && shares >= targetShares) _graduate(); } /// @notice Anyone may nudge a curve that has already met its bar but has /// not traded since. It cannot graduate one that has not. function poke() external nonReentrant { if (!graduated) _graduateIfDue(); } function _graduate() internal { graduated = true; poolEth = raised; raised = 0; // The reserved 200M plus anything the curve never sold. Derived from // tokensSold rather than read from the balance: graduation happens // inside a buy, before that buyer's tokens have been handed over, so a // balance read here would count them twice. poolTokens = SUPPLY - tokensSold; emit Graduated(poolEth, poolTokens, tokensSold, block.timestamp); } function _pay(address to, uint256 v) internal { if (v == 0) return; (bool ok, ) = to.call{value: v}(""); if (!ok) revert PayFailed(); } /// @dev No bare transfers. Coin arrives through buy(), or not at all, so /// the balance always matches what the curve thinks it holds. receive() external payable { revert NoBareTransfers(); } } /// @title StockPairedLaunchpad /// @notice Deploys one curve per pair. The company and the app are written /// into the curve as immutable values at launch, so a pair cannot be /// relabelled afterwards by its creator, by the pad, or by anyone /// else. The pad keeps no key over a deployed curve: it cannot pause /// it, drain it, or change a fee. contract StockPairedLaunchpad { uint256 public constant TARGET_USD_E8 = 5_000e8; StockOracle public immutable oracle; address public owner; address public treasury; uint256 public launchFee; struct Launch { address curve; address token; address creator; bytes32 ticker; bytes32 app; uint256 targetShares; uint256 createdAt; } Launch[] private launches; mapping(address => uint256) public launchIdOfCurve; // 1-based, 0 means unknown mapping(address => uint256) public launchIdOfToken; event Launched( uint256 indexed id, address indexed curve, address indexed token, address creator, bytes32 ticker, bytes32 app, uint256 targetShares, uint256 targetWei, string name, string symbol ); event TreasurySet(address treasury); event LaunchFeeSet(uint256 fee); event OwnerSet(address owner); error NotOwner(); error FeeShort(); error BadSymbol(); error BadName(); error BadUri(); error ZeroAddress(); error PayFailed(); modifier onlyOwner() { if (msg.sender != owner) revert NotOwner(); _; } constructor(StockOracle _oracle, address _treasury, uint256 _launchFee) { if (address(_oracle) == address(0) || _treasury == address(0)) revert ZeroAddress(); oracle = _oracle; owner = msg.sender; treasury = _treasury; launchFee = _launchFee; emit OwnerSet(msg.sender); emit TreasurySet(_treasury); emit LaunchFeeSet(_launchFee); } function setOwner(address o) external onlyOwner { if (o == address(0)) revert ZeroAddress(); owner = o; emit OwnerSet(o); } /// @dev Only affects pairs launched afterwards. A deployed curve's /// treasury is immutable. function setTreasury(address t) external onlyOwner { if (t == address(0)) revert ZeroAddress(); treasury = t; emit TreasurySet(t); } function setLaunchFee(uint256 f) external onlyOwner { launchFee = f; emit LaunchFeeSet(f); } function launchCount() external view returns (uint256) { return launches.length; } function launchAt(uint256 id) external view returns (Launch memory) { return launches[id]; } function curveOf(uint256 id) external view returns (address) { return launches[id].curve; } function tokenOf(uint256 id) external view returns (address) { return launches[id].token; } /// @notice What a launch would cost and aim at right now, so the form can /// show real numbers before anyone signs. function preview(bytes32 ticker) external view returns (uint256 targetShares, uint256 targetWei, uint256 sharePriceUsdE8, uint256 nativeUsdE8, uint256 fee) { (sharePriceUsdE8, ) = oracle.priceOf(ticker); (nativeUsdE8, ) = oracle.nativeUsd(); targetShares = (TARGET_USD_E8 * 1e18) / sharePriceUsdE8; targetWei = (TARGET_USD_E8 * 1e18) / nativeUsdE8; fee = launchFee; } /// @notice Launch SYMBOL/TICKER. Pays `launchFee` to the treasury; whatever /// is sent above it becomes the creator's first buy on the fresh /// curve, in the same transaction. function launch( string calldata name, string calldata symbol, string calldata metadataURI, bytes32 ticker, bytes32 app, uint256 minTokensOut ) external payable returns (uint256 id, address curve, address token) { if (msg.value < launchFee) revert FeeShort(); _checkName(name); _checkSymbol(symbol); if (bytes(metadataURI).length > 300) revert BadUri(); // Strict reads: a launch that cannot be priced is refused rather than // pinned to a number nobody has refreshed. (uint256 shareUsd, ) = oracle.priceOf(ticker); (uint256 nativeUsd, ) = oracle.nativeUsd(); uint256 targetShares = (TARGET_USD_E8 * 1e18) / shareUsd; uint256 targetWei = (TARGET_USD_E8 * 1e18) / nativeUsd; StockPairedCurve c = new StockPairedCurve( oracle, treasury, msg.sender, ticker, app, targetShares, targetWei, shareUsd, name, symbol, metadataURI ); curve = address(c); token = address(c.token()); id = launches.length; launches.push(Launch(curve, token, msg.sender, ticker, app, targetShares, block.timestamp)); launchIdOfCurve[curve] = id + 1; launchIdOfToken[token] = id + 1; emit Launched(id, curve, token, msg.sender, ticker, app, targetShares, targetWei, name, symbol); uint256 fee = launchFee; if (fee > 0) { (bool ok, ) = treasury.call{value: fee}(""); if (!ok) revert PayFailed(); } uint256 first = msg.value - fee; if (first > 0) c.buyFor{value: first}(msg.sender, minTokensOut, block.timestamp); } function _checkName(string calldata n) private pure { uint256 len = bytes(n).length; if (len == 0 || len > 40) revert BadName(); } function _checkSymbol(string calldata s) private pure { bytes memory b = bytes(s); if (b.length < 2 || b.length > 10) revert BadSymbol(); for (uint256 i = 0; i < b.length; i++) { bytes1 ch = b[i]; bool digit = ch >= 0x30 && ch <= 0x39; bool upper = ch >= 0x41 && ch <= 0x5a; if (!digit && !upper) revert BadSymbol(); } } }