# Bodhi v3 on Base solc 0.8.24+commit.e11b9ed9, evm cancun, via_ir, optimizer 200 runs solady 90db92ce173856605d24a554969f2c67cadbc7e9 BodhiV3 0x2e197fA835C41dAE2BD43a6333ADB312fcE262Ea Funding 0xd370B7be70B28E79d31Bd8223EB5300CF0C35C2B CacaoName 0xdec7711E8954618f61E059B2cb00012E93eC39d6 // ===== src/BodhiV3.sol ===== // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Token} from "./Token.sol"; import {Market} from "./Market.sol"; contract BodhiV3 { mapping(address token => address market) public tokenToMarket; event TokenCreated( address indexed token, address indexed market, string name, string symbol, address indexed creator ); function createToken(string memory name, string memory symbol) external payable returns (address token, address market) { token = address(new Token(name, symbol, address(this))); market = address(new Market(Token(token))); Token(token).transfer(market, Token(token).totalSupply()); tokenToMarket[token] = market; emit TokenCreated(token, market, name, symbol, msg.sender); if (msg.value > 0) { // Fixed initial reserves and no external callbacks before this atomic first buy. Market(market).buy{value: msg.value}(msg.sender, 0); } } } // ===== src/Market.sol ===== // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Token} from "./Token.sol"; import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; import {ERC20} from "solady/tokens/ERC20.sol"; import {FixedPointMathLib as Math} from "solady/utils/FixedPointMathLib.sol"; // Curve Phase: (x + vx)(y + vy) = k (750M tokens) // Pool Phase: x * y = k (250M tokens + 4 ETH) // Virtual reserves let the curve start without ETH; the pool opens at the curve's final price. contract Market is ReentrancyGuard, ERC20 { Token public immutable token; uint256 public constant CURVE_TOKENS = 750_000_000e18; uint256 public constant POOL_TOKENS = 250_000_000e18; uint256 public constant VIRTUAL_TOKEN_RESERVE = 125_000_000e18; uint256 public constant VIRTUAL_ETH_RESERVE = 2 ether; uint256 public constant CURVE_K = VIRTUAL_ETH_RESERVE * (CURVE_TOKENS + POOL_TOKENS + VIRTUAL_TOKEN_RESERVE); uint256 public constant CURVE_ETH = (CURVE_K / (POOL_TOKENS + VIRTUAL_TOKEN_RESERVE)) - VIRTUAL_ETH_RESERVE; uint256 public tokenReserve = CURVE_TOKENS + POOL_TOKENS; uint256 public ethReserve; bool public poolOpen; error ZeroInput(); error InvalidRecipient(); error ZeroOutput(); error SlippageToleranceExceeded(); error PoolNotOpen(); event Trade( address indexed sender, address indexed to, bool isBuy, uint256 ethAmount, uint256 tokenAmount, uint256 tokenReserve, uint256 ethReserve ); event PoolOpened(uint256 tokenReserve, uint256 ethReserve); event LiquidityAdded(address indexed provider, uint256 liquidity, uint256 tokenIn, uint256 ethIn); event LiquidityRemoved(address indexed provider, uint256 liquidity, uint256 tokenOut, uint256 ethOut); constructor(Token token_) { token = token_; } function name() public pure override returns (string memory) { return "Bodhi LP"; } function symbol() public pure override returns (string memory) { return "BODHI-LP"; } function _givePermit2InfiniteAllowance() internal pure override returns (bool) { return false; } // Round in favor of the market: LP deposits round up, withdrawals and trade outputs round down. function quoteAddLiquidity(uint256 liquidity) public view returns (uint256 tokenIn, uint256 ethIn) { if (!poolOpen) revert PoolNotOpen(); uint256 supply = totalSupply(); tokenIn = Math.fullMulDivUp(liquidity, tokenReserve, supply); ethIn = Math.fullMulDivUp(liquidity, ethReserve, supply); } function addLiquidity(uint256 liquidity, uint256 maxTokenIn) external payable nonReentrant returns (uint256 tokenIn, uint256 ethIn) { if (liquidity == 0) revert ZeroInput(); (tokenIn, ethIn) = quoteAddLiquidity(liquidity); if (tokenIn > maxTokenIn || ethIn > msg.value) revert SlippageToleranceExceeded(); tokenReserve += tokenIn; ethReserve += ethIn; _mint(msg.sender, liquidity); SafeTransferLib.safeTransferFrom(address(token), msg.sender, address(this), tokenIn); emit LiquidityAdded(msg.sender, liquidity, tokenIn, ethIn); if (msg.value > ethIn) SafeTransferLib.safeTransferETH(msg.sender, msg.value - ethIn); } function quoteRemoveLiquidity(uint256 liquidity) public view returns (uint256 tokenOut, uint256 ethOut) { if (!poolOpen) revert PoolNotOpen(); uint256 supply = totalSupply(); tokenOut = Math.fullMulDiv(liquidity, tokenReserve, supply); ethOut = Math.fullMulDiv(liquidity, ethReserve, supply); } function removeLiquidity(uint256 liquidity, uint256 minTokenOut, uint256 minEthOut) external nonReentrant returns (uint256 tokenOut, uint256 ethOut) { if (liquidity == 0) revert ZeroInput(); (tokenOut, ethOut) = quoteRemoveLiquidity(liquidity); if (tokenOut == 0 || ethOut == 0) revert ZeroOutput(); if (tokenOut < minTokenOut || ethOut < minEthOut) revert SlippageToleranceExceeded(); _burn(msg.sender, liquidity); tokenReserve -= tokenOut; ethReserve -= ethOut; emit LiquidityRemoved(msg.sender, liquidity, tokenOut, ethOut); SafeTransferLib.safeTransfer(address(token), msg.sender, tokenOut); SafeTransferLib.safeTransferETH(msg.sender, ethOut); } function quoteBuy(uint256 ethIn) public view returns (uint256 tokenOut) { if (ethIn == 0) return 0; if (poolOpen) return _poolAmountOut(ethIn, ethReserve, tokenReserve); uint256 curveIn = Math.min(ethIn, CURVE_ETH - ethReserve); uint256 nextTokenReserve = Math.divUp(CURVE_K, ethReserve + curveIn + VIRTUAL_ETH_RESERVE) - VIRTUAL_TOKEN_RESERVE; tokenOut = tokenReserve - nextTokenReserve; if (ethIn > curveIn) { tokenOut += _poolAmountOut(ethIn - curveIn, CURVE_ETH, nextTokenReserve); } } function buy(address to, uint256 minTokenOut) external payable nonReentrant returns (uint256 tokenOut) { if (to == address(0) || to == address(this) || to == address(token)) revert InvalidRecipient(); if (msg.value == 0) revert ZeroInput(); tokenOut = quoteBuy(msg.value); if (tokenOut == 0) revert ZeroOutput(); if (tokenOut < minTokenOut) revert SlippageToleranceExceeded(); if (!poolOpen && ethReserve + msg.value >= CURVE_ETH) _openPool(); tokenReserve -= tokenOut; ethReserve += msg.value; SafeTransferLib.safeTransfer(address(token), to, tokenOut); emit Trade(msg.sender, to, true, msg.value, tokenOut, tokenReserve, ethReserve); } function quoteSell(uint256 tokenIn) public view returns (uint256 ethOut) { if (tokenIn == 0) return 0; if (poolOpen) return _poolAmountOut(tokenIn, tokenReserve, ethReserve); uint256 raw = Math.divUp(CURVE_K, tokenReserve + tokenIn + VIRTUAL_TOKEN_RESERVE); uint256 nextEthReserve = raw > VIRTUAL_ETH_RESERVE ? raw - VIRTUAL_ETH_RESERVE : 0; return ethReserve - nextEthReserve; } function sell(uint256 tokenIn, uint256 minEthOut) external nonReentrant returns (uint256 ethOut) { if (tokenIn == 0) revert ZeroInput(); ethOut = quoteSell(tokenIn); if (ethOut == 0) revert ZeroOutput(); if (ethOut < minEthOut) revert SlippageToleranceExceeded(); tokenReserve += tokenIn; ethReserve -= ethOut; SafeTransferLib.safeTransferFrom(address(token), msg.sender, address(this), tokenIn); emit Trade(msg.sender, msg.sender, false, ethOut, tokenIn, tokenReserve, ethReserve); SafeTransferLib.safeTransferETH(msg.sender, ethOut); } function _poolAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) private pure returns (uint256) { uint256 adjustedIn = amountIn * 997; return Math.fullMulDiv(reserveOut, adjustedIn, reserveIn * 1000 + adjustedIn); } // At ethReserve == CURVE_ETH, CURVE_K divides evenly, leaving // exactly POOL_TOKENS regardless of prior trade history. function _openPool() private { poolOpen = true; _mint(address(0), Math.sqrt(POOL_TOKENS * CURVE_ETH)); emit PoolOpened(POOL_TOKENS, CURVE_ETH); } } // ===== src/Token.sol ===== // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {ERC20} from "solady/tokens/ERC20.sol"; contract Token is ERC20 { string private _name; string private _symbol; bytes32 private immutable _NAME_HASH; constructor(string memory tokenName, string memory tokenSymbol, address mintTo) { _name = tokenName; _symbol = tokenSymbol; _NAME_HASH = keccak256(bytes(tokenName)); _mint(mintTo, 1_000_000_000e18); } function name() public view override returns (string memory) { return _name; } function symbol() public view override returns (string memory) { return _symbol; } function _givePermit2InfiniteAllowance() internal pure override returns (bool) { return false; } // Avoid recomputing keccak256(bytes(name())) in DOMAIN_SEPARATOR/permit. function _constantNameHash() internal view override returns (bytes32) { return _NAME_HASH; } } // ===== src/Funding.sol ===== // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {ReentrancyGuard} from "solady/utils/ReentrancyGuard.sol"; import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; // Funding v2: tokens go directly to creators, without escrow or claims. contract Funding is ReentrancyGuard { struct Action { address creator; string uri; } mapping(address token => mapping(uint256 actionId => Action)) public actions; mapping(address token => uint256) public actionCount; event ActionSubmitted(address indexed token, uint256 indexed actionId, address indexed creator, string uri); event Funded(address indexed token, uint256 indexed actionId, address indexed funder, uint256 amount); error ZeroAmount(); error ActionNotFound(); error URIRequired(); function submit(address token, string calldata uri) external returns (uint256 actionId) { if (bytes(uri).length == 0) revert URIRequired(); actionId = ++actionCount[token]; actions[token][actionId] = Action({creator: msg.sender, uri: uri}); emit ActionSubmitted(token, actionId, msg.sender, uri); } function fund(address token, uint256 actionId, uint256 amount) external nonReentrant { if (amount == 0) revert ZeroAmount(); Action storage action = actions[token][actionId]; if (action.creator == address(0)) revert ActionNotFound(); SafeTransferLib.safeTransferFrom(token, msg.sender, action.creator, amount); emit Funded(token, actionId, msg.sender, amount); } } // ===== src/periphery/CacaoName.sol ===== // SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract CacaoName { mapping(address => string) public names; event NameSet(address indexed addr, string name); function setName(string calldata name) external { names[msg.sender] = name; emit NameSet(msg.sender, name); } }