// SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.26; interface ISwapRouter02 { struct ExactInputParams { bytes path; address recipient; uint256 amountIn; uint256 amountOutMinimum; } function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut); } interface IERC20Min { function balanceOf(address) external view returns (uint256); } interface IWETH { function deposit() external payable; function withdraw(uint256) external; } /// @title KeelCoin /// @notice A coin whose whole market is held in one tokenized stock. /// /// Every coin is its own constant-product market: the 1B coins on one side, and on the other a reserve of ONE /// Robinhood stock token (a virtual amount set at launch, plus every share buyers have paid in). There is no ETH in /// it at all. A buy swaps whatever the buyer pays with (ETH, USDG, anything with a Uniswap route) into the stock in /// the same transaction and buys coins with the shares; a buy can also pay in the stock itself. A sell takes shares /// out of the reserve and pays them out as the stock, or swaps them back to ETH or USDG on the way out. /// /// So the price of a coin is a number of SHARES, and everything the market holds is stock: if the stock rises, so /// does every coin's dollar price and every seller's payout. The reserve always covers the whole curve: selling /// every coin back takes it to exactly what the fees left in it, never below zero. /// /// Fee: 1% of every trade, taken in the stock. Half stays in the reserve (it raises the price for every holder), /// half accrues to the coin's creator, who can claim it any time. No owner, no pause, no upgrade, no fee to anyone /// else. Nothing here can be changed after launch. contract KeelCoin { // ------------------------------------------------------------------ ERC-20 string public name; string public symbol; 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); // ------------------------------------------------------------------ constants uint256 public constant SUPPLY = 1_000_000_000e18; /// @notice The whole trade fee, in basis points, taken in the stock on every buy and sell. uint256 public constant FEE_BPS = 100; /// @notice The creator's part of the fee, in basis points of the trade; the rest stays in the reserve. uint256 public constant CREATOR_BPS = 50; ISwapRouter02 public immutable router; address public immutable weth; address public immutable factory; // ------------------------------------------------------------------ set once at launch address public creator; /// @notice The stock token the whole market is held in. address public stock; /// @notice The Uniswap fee tier of the USDG/stock pool the page routes through by default (a hint; any route works). uint24 public stockPoolFee; uint64 public launchedAt; /// @notice Shares (raw stock units) the curve starts with but nobody paid in. They set the starting price. uint256 public virtualStock; /// @notice Picture, description and links, stored as contract code (SSTORE2) by the factory. address public metaPointer; // ------------------------------------------------------------------ state /// @notice Coins still held by the curve. Kept apart from `balanceOf[this]`, so coins sent here by mistake /// can never move the price. uint256 public curveCoins; /// @notice Real shares in the reserve: what buyers paid in, less what sellers took out, plus the reserve's half of fees. uint256 public reserveStock; /// @notice Shares owed to the creator and not yet claimed. uint256 public creatorStock; /// @notice Lifetime: shares paid to the creator, shares traded through the curve (both ways), fees, trades. uint256 public creatorPaid; uint256 public volumeStock; uint256 public totalFees; uint256 public tradeCount; uint256 private _locked; /// @param payToken what the trader paid with (buy) or was paid in (sell): address(0) is ETH, `stock` is the stock itself /// @param payAmount how much of it /// @param stockAmount shares into the curve (buy, before the fee) or out of it (sell, after the fee) event Trade( address indexed trader, address indexed to, bool isBuy, uint256 coinAmount, uint256 stockAmount, uint256 fee, address payToken, uint256 payAmount, uint256 stockReserve, uint256 coinReserve ); event CreatorClaim(address indexed creator, uint256 amount); event CreatorChanged(address indexed from, address indexed to); error AlreadyInitialized(); error OnlyFactory(); error OnlyCreator(); error Reentrancy(); error ZeroAmount(); error Slippage(); error BadPath(); error BadAmount(); error EthTransferFailed(); error TokenTransferFailed(); error InsufficientBalance(); error InsufficientAllowance(); error NothingToClaim(); modifier nonReentrant() { if (_locked == 1) revert Reentrancy(); _locked = 1; _; _locked = 0; } constructor(address router_, address weth_, address factory_) { router = ISwapRouter02(router_); weth = weth_; factory = factory_; launchedAt = type(uint64).max; // the implementation itself can never be initialised } function initialize( string calldata name_, string calldata symbol_, address metaPointer_, address creator_, address stock_, uint24 stockPoolFee_, uint256 virtualStock_ ) external { if (msg.sender != factory) revert OnlyFactory(); if (launchedAt != 0) revert AlreadyInitialized(); name = name_; symbol = symbol_; metaPointer = metaPointer_; creator = creator_; stock = stock_; stockPoolFee = stockPoolFee_; virtualStock = virtualStock_; launchedAt = uint64(block.timestamp); totalSupply = SUPPLY; curveCoins = SUPPLY; balanceOf[address(this)] = SUPPLY; emit Transfer(address(0), address(this), SUPPLY); } // ------------------------------------------------------------------ ERC-20 logic function transfer(address to, uint256 value) external returns (bool) { _transfer(msg.sender, to, value); return true; } function approve(address spender, uint256 value) external returns (bool) { allowance[msg.sender][spender] = value; emit Approval(msg.sender, spender, 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) { if (a < value) revert InsufficientAllowance(); allowance[from][msg.sender] = a - value; } _transfer(from, to, value); return true; } function _transfer(address from, address to, uint256 value) internal { uint256 b = balanceOf[from]; if (b < value) revert InsufficientBalance(); unchecked { balanceOf[from] = b - value; balanceOf[to] += value; } emit Transfer(from, to, value); } // ------------------------------------------------------------------ the curve /// @notice The curve's reserves: shares (virtual + real) and coins. function reserves() public view returns (uint256 stockReserve, uint256 coinReserve) { return (virtualStock + reserveStock, curveCoins); } /// @notice Coins out for `stockIn` shares paid into the curve, and the fee taken from them. function quoteBuy(uint256 stockIn) public view returns (uint256 coinsOut, uint256 fee) { fee = stockIn * FEE_BPS / 10_000; uint256 net = stockIn - fee; (uint256 x, uint256 y) = reserves(); coinsOut = y * net / (x + net); } /// @notice Shares paid out for selling `coinsIn`, and the fee taken from them. function quoteSell(uint256 coinsIn) public view returns (uint256 stockOut, uint256 fee) { (uint256 x, uint256 y) = reserves(); uint256 gross = x * coinsIn / (y + coinsIn); if (gross > reserveStock) gross = reserveStock; fee = gross * FEE_BPS / 10_000; stockOut = gross - fee; } /// @notice Buy coins. /// - Pay in ETH: send it as value, `path` is a Uniswap v3 path from WETH to the stock, `amountIn` = msg.value. /// - Pay in the stock: no value, empty `path`, `amountIn` shares (approve this contract first). /// - Pay in any other token: no value, `path` from that token to the stock (approve this contract first). /// `minCoinsOut` is the only price check needed: it bounds the swap and the curve together. function buy(bytes calldata path, uint256 amountIn, uint256 minCoinsOut, address to) external payable nonReentrant returns (uint256 coinsOut) { if (amountIn == 0) revert ZeroAmount(); address s = stock; address payToken; uint256 stockIn; if (msg.value > 0) { if (amountIn != msg.value) revert BadAmount(); _checkPath(path, weth, s); IWETH(weth).deposit{value: msg.value}(); stockIn = _swap(path, weth, msg.value, s); } else if (path.length == 0) { payToken = s; stockIn = _pull(s, amountIn); } else { payToken = _first(path); _checkPath(path, payToken, s); stockIn = _swap(path, payToken, _pull(payToken, amountIn), s); } if (stockIn == 0) revert ZeroAmount(); uint256 fee; (coinsOut, fee) = quoteBuy(stockIn); if (coinsOut == 0 || coinsOut < minCoinsOut) revert Slippage(); uint256 toCreator = stockIn * CREATOR_BPS / 10_000; reserveStock += stockIn - toCreator; creatorStock += toCreator; curveCoins -= coinsOut; totalFees += fee; volumeStock += stockIn; tradeCount++; _transfer(address(this), to, coinsOut); (uint256 x, uint256 y) = reserves(); emit Trade(msg.sender, to, true, coinsOut, stockIn, fee, payToken, amountIn, x, y); } /// @notice Sell coins. /// - Be paid in the stock: empty `path`. /// - Be paid in ETH: `path` from the stock to WETH (it is unwrapped and sent as ETH). /// - Be paid in another token: `path` from the stock to that token. /// `minOut` is in whatever the seller is paid in. function sell(uint256 coinsIn, bytes calldata path, uint256 minOut, address to) external nonReentrant returns (uint256 out) { if (coinsIn == 0) revert ZeroAmount(); (uint256 stockOut, uint256 fee) = quoteSell(coinsIn); if (stockOut == 0) revert Slippage(); _transfer(msg.sender, address(this), coinsIn); curveCoins += coinsIn; uint256 toCreator = (stockOut + fee) * CREATOR_BPS / 10_000; reserveStock -= stockOut + toCreator; creatorStock += toCreator; totalFees += fee; volumeStock += stockOut + fee; tradeCount++; address s = stock; address payToken; if (path.length == 0) { payToken = s; out = stockOut; if (out < minOut) revert Slippage(); _send(s, to, out); } else { payToken = _last(path); _checkPath(path, s, payToken); out = _swap(path, s, stockOut, payToken); if (out < minOut) revert Slippage(); if (payToken == weth) { IWETH(weth).withdraw(out); payToken = address(0); (bool ok,) = to.call{value: out}(""); if (!ok) revert EthTransferFailed(); } else { _send(payToken, to, out); } } (uint256 x, uint256 y) = reserves(); emit Trade(msg.sender, to, false, coinsIn, stockOut, fee, payToken, out, x, y); } // ------------------------------------------------------------------ the creator's half of the fee /// @notice Pay the creator what they are owed, in the stock. Anyone may call it; it always pays the creator. function claim() external nonReentrant returns (uint256 amount) { amount = creatorStock; if (amount == 0) revert NothingToClaim(); creatorStock = 0; creatorPaid += amount; address c = creator; _send(stock, c, amount); emit CreatorClaim(c, amount); } /// @notice The creator can hand their share of future fees (and what is owed now) to another address. function setCreator(address next) external { if (msg.sender != creator) revert OnlyCreator(); if (next == address(0)) revert BadAmount(); emit CreatorChanged(creator, next); creator = next; } // ------------------------------------------------------------------ swaps /// @dev Swap exactly `amountIn` of `tokenIn` along `path` into this contract, and return what ARRIVED of /// `tokenOut` (measured, so a route that pays less than it says cannot be over-credited). The router is /// approved for exactly this amount and the approval is cleared afterwards. function _swap(bytes calldata path, address tokenIn, uint256 amountIn, address tokenOut) internal returns (uint256 got) { uint256 before = IERC20Min(tokenOut).balanceOf(address(this)); if (!_callOk(tokenIn, abi.encodeWithSelector(0x095ea7b3, address(router), amountIn))) revert TokenTransferFailed(); router.exactInput(ISwapRouter02.ExactInputParams(path, address(this), amountIn, 0)); _callOk(tokenIn, abi.encodeWithSelector(0x095ea7b3, address(router), 0)); got = IERC20Min(tokenOut).balanceOf(address(this)) - before; } /// @dev Pull `amount` of `token` from the sender; returns what arrived. function _pull(address token, uint256 amount) internal returns (uint256 got) { uint256 before = IERC20Min(token).balanceOf(address(this)); if (!_callOk(token, abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), amount))) { revert TokenTransferFailed(); } got = IERC20Min(token).balanceOf(address(this)) - before; } function _send(address token, address to, uint256 amount) internal { if (!_callOk(token, abi.encodeWithSelector(0xa9059cbb, to, amount))) revert TokenTransferFailed(); } /// @dev A v3 path is token (20) [fee (3) token (20)]+. It must run from `a` to `b`, have at least one hop, and /// never start or end at this coin (whose curve balance is kept apart and must not be moved by a route). function _checkPath(bytes calldata path, address a, address b) internal view { if (path.length < 43 || (path.length - 20) % 23 != 0) revert BadPath(); if (_first(path) != a || _last(path) != b || a == b) revert BadPath(); if (a == address(this) || b == address(this)) revert BadPath(); } function _first(bytes calldata path) internal pure returns (address t) { if (path.length < 20) revert BadPath(); t = address(bytes20(path[:20])); } function _last(bytes calldata path) internal pure returns (address t) { if (path.length < 20) revert BadPath(); t = address(bytes20(path[path.length - 20:])); } function _callOk(address target, bytes memory data) internal returns (bool) { if (target.code.length == 0) return false; (bool ok, bytes memory ret) = target.call(data); return ok && (ret.length == 0 || (ret.length >= 32 && abi.decode(ret, (bool)))); } // ------------------------------------------------------------------ for the app /// @notice ABI-encoded (string image, string description, string website, string x, string telegram). function meta() public view returns (bytes memory data) { address p = metaPointer; if (p == address(0)) return data; uint256 size = p.code.length; if (size <= 1) return data; data = new bytes(size - 1); assembly ("memory-safe") { extcodecopy(p, add(data, 32), 1, sub(size, 1)) } } struct Info { string name; string symbol; address creator; address stock; uint24 stockPoolFee; uint64 launchedAt; uint256 totalSupply; uint256 virtualStock; uint256 reserveStock; uint256 curveCoins; uint256 creatorStock; uint256 creatorPaid; uint256 volumeStock; uint256 totalFees; uint256 tradeCount; uint256 heldStock; } function info() external view returns (Info memory i) { i = Info( name, symbol, creator, stock, stockPoolFee, launchedAt, totalSupply, virtualStock, reserveStock, curveCoins, creatorStock, creatorPaid, volumeStock, totalFees, tradeCount, IERC20Min(stock).balanceOf(address(this)) ); } /// @dev Only WETH (an unwrap during a sell) may send ETH here. receive() external payable { if (msg.sender != weth) revert(); } }