StockFactory
0x4714ca6ee4ffcf7c75614d203ff6736c4862e34c
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
16,578 bytes
ABI entries
56
29 read · 16 write
License
none
Contract information
- Address
- 0x4714ca6ee4ffcf7c75614d203ff6736c4862e34c
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0xAb882B959F…317d83d1f5
- Creation tx
- 0x083151a8e9…111556db7a
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (29)
FACTORY() → address
POSITION_MANAGER() → address
SWAP_ROUTER() → address
WBNB() → address
_toHex(bytes) → string
buybackPool() → address
calculateSqrtPriceX96(uint256, uint256, uint256) → uint160
conveyor() → address
deployCoinEnabled() → bool
deployedTokens(uint256) → address, string, string, address, address, uint256, string, uint256
deployerTokens(address, uint256) → address
devAddress() → address
getAllTokensByDeployer(address) → address[]
getTokenFeesClaimed(address) → uint256
getTokenFeesGenerated(address) → uint256
getTokenMarketCapInQuote(address) → uint256
getTokenStats(address) → uint256, uint256, address, address
indexer() → address
platformController() → address
quoteTokenOf(address) → address
stockConfigCount() → uint256
stockConfigs(uint256) → address, uint160, int24, int24, uint256, uint256, bool
tokenCount() → uint256
tokenCreators(address) → address
tokenFeesClaimed(address) → uint256
tokenFeesGenerated(address) → uint256
tokenInfoByAddress(address) → address, string, string, address, address, uint256, string, uint256
tokenToNFTId(address) → uint256
tokenToPool(address) → address
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "_indexer",
"type": "address"
},
{
"internalType": "address",
"name": "_buybackPool",
"type": "address"
},
{
"internalType": "address",
"name": "_devAddress",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "DeploymentDisabled",
"type": "error"
},
{
"inputs": [],
"name": "InvalidConfigId",
"type": "error"
},
{
"inputs": [],
"name": "NoBNBToWithdraw",
"type": "error"
},
{
"inputs": [],
"name": "NoPosition",
"type": "error"
},
{
"inputs": [],
"name": "NotController",
"type": "error"
},
{
"inputs": [],
"name": "PoolTokenMismatch",
"type": "error"
},
{
"inputs": [],
"name": "QuoteTokenNotSet",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [],
"name": "TokenMustBeToken0",
"type": "error"
},
{
"inputs": [],
"name": "FACTORY",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "POSITION_MANAGER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "SWAP_ROUTER",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "WBNB",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "buffer",
"type": "bytes"
}
],
"name": "_toHex",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_quoteToken",
"type": "address"
},
{
"internalType": "uint160",
"name": "_sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "_tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "_tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "_quoteAmountDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_tokenAmountDesired",
"type": "uint256"
}
],
"name": "addStockConfig",
"outputs": [
{
"internalType": "uint256",
"name": "configId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_quoteToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "totalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "targetMcapUsd",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "quoteTokenUsdPrice",
"type": "uint256"
},
{
"internalType": "int24",
"name": "_tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "_tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "_quoteAmountDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_tokenAmountDesired",
"type": "uint256"
}
],
"name": "addStockConfigForTargetMcap",
"outputs": [
{
"internalType": "uint256",
"name": "configId",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "buybackPool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "totalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "targetMcapUsd",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "quoteTokenUsdPrice",
"type": "uint256"
}
],
"name": "calculateSqrtPriceX96",
"outputs": [
{
"internalType": "uint160",
"name": "",
"type": "uint160"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
}
],
"name": "collectStockFeesAndBuyback",
"outputs": [
{
"internalType": "uint256",
"name": "tokenFeesCollected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "quoteFeesCollected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "creatorQuotePaid",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "conveyor",
"outputs": [
{
"internalType": "contract StockFactoryConveyor",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "deployCoinEnabled",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
},
{
"internalType": "string",
"name": "_metadata",
"type": "string"
},
{
"internalType": "bytes32",
"name": "saltSeed",
"type": "bytes32"
},
{
"internalType": "uint256",
"name": "stockConfigId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "initialBuyQuoteAmount",
"type": "uint256"
}
],
"name": "deployStockCoin",
"outputs": [
{
"internalType": "uint256",
"name": "tokensReceived",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "deployedTokens",
"outputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "time",
"type": "uint256"
},
{
"internalType": "string",
"name": "metadata",
"type": "string"
},
{
"internalType": "uint256",
"name": "nftId",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "deployerTokens",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "devAddress",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_deployer",
"type": "address"
}
],
"name": "getAllTokensByDeployer",
"outputs": [
{
"internalType": "address[]",
"name": "",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
}
],
"name": "getTokenFeesClaimed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
}
],
"name": "getTokenFeesGenerated",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
}
],
"name": "getTokenMarketCapInQuote",
"outputs": [
{
"internalType": "uint256",
"name": "marketCapInQuote",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
}
],
"name": "getTokenStats",
"outputs": [
{
"internalType": "uint256",
"name": "marketCapInQuote",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "totalFeesGenerated",
"type": "uint256"
},
{
"internalType": "address",
"name": "creatorAddress",
"type": "address"
},
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "indexer",
"outputs": [
{
"internalType": "contract IIndexer",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "platformController",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "quoteTokenOf",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueERC20",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pool",
"type": "address"
}
],
"name": "setBuybackPool",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_conveyor",
"type": "address"
}
],
"name": "setConveyor",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_devAddress",
"type": "address"
}
],
"name": "setDevAddress",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_factory",
"type": "address"
}
],
"name": "setFactory",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_newIndexer",
"type": "address"
}
],
"name": "setIndexer",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "configId",
"type": "uint256"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"name": "setStockConfigEnabled",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "newCreator",
"type": "address"
}
],
"name": "setTokenCreator",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "stockConfigCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "stockConfigs",
"outputs": [
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
},
{
"internalType": "uint160",
"name": "sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "quoteAmountDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "tokenAmountDesired",
"type": "uint256"
},
{
"internalType": "bool",
"name": "enabled",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "toggleDeployCoin",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "tokenCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenCreators",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenFeesClaimed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenFeesGenerated",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenInfoByAddress",
"outputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
},
{
"internalType": "string",
"name": "name",
"type": "string"
},
{
"internalType": "string",
"name": "symbol",
"type": "string"
},
{
"internalType": "address",
"name": "deployer",
"type": "address"
},
{
"internalType": "address",
"name": "quoteToken",
"type": "address"
},
{
"internalType": "uint256",
"name": "time",
"type": "uint256"
},
{
"internalType": "string",
"name": "metadata",
"type": "string"
},
{
"internalType": "uint256",
"name": "nftId",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenToNFTId",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "tokenToPool",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "configId",
"type": "uint256"
},
{
"internalType": "uint160",
"name": "_sqrtPriceX96",
"type": "uint160"
},
{
"internalType": "int24",
"name": "_tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "_tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "_quoteAmountDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_tokenAmountDesired",
"type": "uint256"
}
],
"name": "updateStockConfig",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "configId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "totalSupply",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "targetMcapUsd",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "quoteTokenUsdPrice",
"type": "uint256"
},
{
"internalType": "int24",
"name": "_tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "_tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "_quoteAmountDesired",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_tokenAmountDesired",
"type": "uint256"
}
],
"name": "updateStockConfigForTargetMcap",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "withdrawFeesBNB",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.9;
import "./Indexer.sol";
import "./StockFactoryConveyor.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
error QuoteTokenNotSet();
error PoolTokenMismatch();
/// @notice Stock-paired launchpad: a sibling to Factory.sol, not a
/// replacement. Factory.sol always pairs a new token's pool against WBNB;
/// this contract pairs it against an admin-whitelisted tokenized stock
/// (TSLA, AAPL, SPY, ...) instead, so the pool - and creator fees - are
/// denominated in that stock rather than the native coin. Deployed
/// separately and authorized on the same Indexer; the live Factory is
/// untouched.
///
/// The creator supplies the quote token (e.g. TSLA) directly, via
/// transferFrom after approving this contract - not ETH. An earlier version
/// of this contract took ETH and swapped it into the quote token internally,
/// but that meant every launch depended on a liquid WBNB/quoteToken pool
/// existing at a specific fee tier. Confirmed on real chain state that this
/// doesn't hold: TSLA's liquid pool is at fee 3000, and AAPL currently has
/// NO liquid WBNB pool at all - a hardcoded/single fee tier can't work, and
/// even a per-config fee tier still fails for stocks with no market at all.
/// Taking the quote token directly sidesteps that entirely: no swap, no
/// slippage/sandwich risk, and it works for any stock the creator can get
/// their hands on, whether or not it has WBNB liquidity on this chain.
contract StockFactory is ReentrancyGuard {
struct TokenInfo {
address tokenAddress;
string name;
string symbol;
address deployer;
address quoteToken;
uint256 time;
string metadata;
uint256 nftId;
}
/// @dev One config per supported stock. sqrtPriceX96 must be calibrated
/// off-chain to that stock's current USD price whenever it moves
/// meaningfully - same limitation the live Factory already has for its
/// single ETH config, just now per-stock instead of a one-time constant.
struct StockLiquidityConfig {
address quoteToken;
uint160 sqrtPriceX96;
int24 tickLower;
int24 tickUpper;
// Deliberately NOT derived from the target mcap math - same as the
// live Factory's own ETH config, this is a small dust amount that
// only partially fills the position at the given tick range; the
// sqrtPriceX96 (not this) is what actually sets the starting mcap.
uint256 quoteAmountDesired;
uint256 tokenAmountDesired; // new token amount seeded on the other side
bool enabled;
}
IIndexer public indexer;
mapping(uint256 => TokenInfo) public deployedTokens;
mapping(address => TokenInfo) public tokenInfoByAddress;
uint256 public tokenCount;
address public platformController;
bool public deployCoinEnabled;
mapping(address => address[]) public deployerTokens;
mapping(address => uint256) public tokenFeesGenerated; // in quote token units
mapping(address => uint256) public tokenFeesClaimed; // in quote token units
mapping(address => uint256) public tokenToNFTId;
mapping(address => address) public tokenToPool;
mapping(address => address) public tokenCreators;
mapping(address => address) public quoteTokenOf; // launched token => the stock it's paired with
mapping(uint256 => StockLiquidityConfig) public stockConfigs;
uint256 public stockConfigCount;
address public constant POSITION_MANAGER = PANCAKE_V3_POSITION_MANAGER;
address public constant WBNB = WBNB_ADDRESS;
address public constant SWAP_ROUTER = PANCAKE_V3_SWAP_ROUTER;
address public FACTORY = PANCAKE_V3_FACTORY;
uint24 private constant FEE_TIER = FEE_TIER_10000; // fee tier for the new token's own pool
address public buybackPool;
address public devAddress;
StockFactoryConveyor public conveyor;
constructor(address _indexer, address _buybackPool, address _devAddress) {
platformController = tx.origin;
indexer = IIndexer(_indexer);
if (_buybackPool != address(0)) {
buybackPool = _buybackPool;
}
if (_devAddress != address(0)) {
devAddress = _devAddress;
}
}
receive() external payable {}
// ---------------------------------------------------------------------
// Admin
// ---------------------------------------------------------------------
function setConveyor(address _conveyor) external {
if (msg.sender != platformController) revert NotController();
conveyor = StockFactoryConveyor(_conveyor);
indexer.authorizeAddress(_conveyor);
conveyor.setIndexer(address(indexer));
}
function setBuybackPool(address pool) external {
if (msg.sender != platformController) revert NotController();
require(pool != address(0), "Pool is zero");
buybackPool = pool;
}
function setDevAddress(address _devAddress) external {
if (msg.sender != platformController) revert NotController();
require(_devAddress != address(0), "Dev address is zero");
devAddress = _devAddress;
}
function setIndexer(address _newIndexer) external {
if (msg.sender != platformController) revert NotController();
indexer = IIndexer(_newIndexer);
if (address(conveyor) != address(0)) {
conveyor.setIndexer(_newIndexer);
}
}
function setFactory(address _factory) external {
if (msg.sender != platformController) revert NotController();
FACTORY = _factory;
}
function toggleDeployCoin() external {
if (msg.sender != platformController) revert NotController();
deployCoinEnabled = !deployCoinEnabled;
}
function setTokenCreator(address token, address newCreator) external {
if (msg.sender != platformController) revert NotController();
tokenCreators[token] = newCreator;
}
/// @notice Whitelists a tokenized stock as a launch quote asset with its
/// own calibrated starting price. Only configs added here can be used -
/// creators cannot pair against an arbitrary address. Pass a
/// pre-computed sqrtPriceX96 - see calculateSqrtPriceX96 if you have a
/// target USD mcap instead, or addStockConfigForTargetMcap to compute
/// and store in one call.
function addStockConfig(
address _quoteToken,
uint160 _sqrtPriceX96,
int24 _tickLower,
int24 _tickUpper,
uint256 _quoteAmountDesired,
uint256 _tokenAmountDesired
) external returns (uint256 configId) {
if (msg.sender != platformController) revert NotController();
return
_writeStockConfig(
stockConfigCount,
_quoteToken,
_sqrtPriceX96,
_tickLower,
_tickUpper,
_quoteAmountDesired,
_tokenAmountDesired
);
}
/// @notice Same as addStockConfig, but derives sqrtPriceX96 from a target
/// USD market cap and the quote token's current USD price instead of you
/// working out the raw value by hand. Every stock has a different,
/// moving price - re-derive and call updateStockConfigForTargetMcap
/// whenever it moves meaningfully. _quoteAmountDesired is NOT derived
/// from this math - see the struct comment - pass a small dust amount,
/// same spirit as the live Factory's own ETH config.
///
/// totalSupply is fixed at 1,000,000 * 1e18 for every launched token
/// today (see StockToken.sol), so pass that unless it ever changes.
///
/// targetMcapUsd and quoteTokenUsdPrice must share the same fixed-point
/// scale (e.g. both USD * 1e8, matching a typical price-feed convention)
/// - only their ratio matters, so the scale itself is arbitrary as long
/// as it's consistent between the two.
function addStockConfigForTargetMcap(
address _quoteToken,
uint256 totalSupply,
uint256 targetMcapUsd,
uint256 quoteTokenUsdPrice,
int24 _tickLower,
int24 _tickUpper,
uint256 _quoteAmountDesired,
uint256 _tokenAmountDesired
) external returns (uint256 configId) {
if (msg.sender != platformController) revert NotController();
uint160 sqrtPriceX96 = calculateSqrtPriceX96(
totalSupply,
targetMcapUsd,
quoteTokenUsdPrice
);
return
_writeStockConfig(
stockConfigCount,
_quoteToken,
sqrtPriceX96,
_tickLower,
_tickUpper,
_quoteAmountDesired,
_tokenAmountDesired
);
}
function updateStockConfig(
uint256 configId,
uint160 _sqrtPriceX96,
int24 _tickLower,
int24 _tickUpper,
uint256 _quoteAmountDesired,
uint256 _tokenAmountDesired
) external {
if (msg.sender != platformController) revert NotController();
if (configId >= stockConfigCount) revert InvalidConfigId();
StockLiquidityConfig storage c = stockConfigs[configId];
c.sqrtPriceX96 = _sqrtPriceX96;
c.tickLower = _tickLower;
c.tickUpper = _tickUpper;
c.quoteAmountDesired = _quoteAmountDesired;
c.tokenAmountDesired = _tokenAmountDesired;
}
/// @notice Recalibrates an existing config from a target USD mcap - call
/// this whenever the stock's real-world price has moved meaningfully
/// since it was last set, instead of recomputing sqrtPriceX96 by hand.
function updateStockConfigForTargetMcap(
uint256 configId,
uint256 totalSupply,
uint256 targetMcapUsd,
uint256 quoteTokenUsdPrice,
int24 _tickLower,
int24 _tickUpper,
uint256 _quoteAmountDesired,
uint256 _tokenAmountDesired
) external {
if (msg.sender != platformController) revert NotController();
if (configId >= stockConfigCount) revert InvalidConfigId();
uint160 sqrtPriceX96 = calculateSqrtPriceX96(
totalSupply,
targetMcapUsd,
quoteTokenUsdPrice
);
StockLiquidityConfig storage c = stockConfigs[configId];
c.sqrtPriceX96 = sqrtPriceX96;
c.tickLower = _tickLower;
c.tickUpper = _tickUpper;
c.quoteAmountDesired = _quoteAmountDesired;
c.tokenAmountDesired = _tokenAmountDesired;
}
function _writeStockConfig(
uint256 configId,
address _quoteToken,
uint160 _sqrtPriceX96,
int24 _tickLower,
int24 _tickUpper,
uint256 _quoteAmountDesired,
uint256 _tokenAmountDesired
) internal returns (uint256) {
require(_quoteToken != address(0), "Quote token is zero");
stockConfigs[configId] = StockLiquidityConfig({
quoteToken: _quoteToken,
sqrtPriceX96: _sqrtPriceX96,
tickLower: _tickLower,
tickUpper: _tickUpper,
quoteAmountDesired: _quoteAmountDesired,
tokenAmountDesired: _tokenAmountDesired,
enabled: true
});
if (configId == stockConfigCount) {
unchecked {
++stockConfigCount;
}
}
return configId;
}
/// @notice The one canonical sqrtPriceX96 calculation for this contract.
/// Given how many raw newToken units exist (totalSupply) and what
/// starting market cap you want in USD, computes the sqrtPriceX96 a pool
/// against `quoteTokenUsdPrice`-priced stock needs to be initialized at.
/// Pure/free to call off-chain as often as needed - every stock has a
/// different, moving price, so there is no single reusable constant the
/// way the live Factory's single ETH config gets away with.
///
/// Verified to reproduce the live Factory's own hardcoded ETH-paired
/// sqrtPriceX96 exactly, given its implied ~$3,453 starting mcap and the
/// ETH price noted in its constructor comment.
function calculateSqrtPriceX96(
uint256 totalSupply,
uint256 targetMcapUsd,
uint256 quoteTokenUsdPrice
) public pure returns (uint160) {
require(quoteTokenUsdPrice > 0, "Quote price is zero");
// mcap expressed in raw quote-token units (18 decimals)
uint256 mcapInQuoteRaw = Math.mulDiv(
targetMcapUsd,
1e18,
quoteTokenUsdPrice
);
require(mcapInQuoteRaw > 0, "Target mcap too small for this price");
// quoteToken is always token0, newToken always token1 (see
// findSaltTokenLTQuote), so Uniswap's price (token1/token0) here is
// "newToken per quoteToken" - the inverse of price-per-token.
uint256 priceX192 = Math.mulDiv(totalSupply, 1 << 192, mcapInQuoteRaw);
uint256 sqrtPrice = Math.sqrt(priceX192);
require(sqrtPrice <= type(uint160).max, "sqrtPriceX96 overflow");
return uint160(sqrtPrice);
}
function setStockConfigEnabled(uint256 configId, bool enabled) external {
if (msg.sender != platformController) revert NotController();
if (configId >= stockConfigCount) revert InvalidConfigId();
stockConfigs[configId].enabled = enabled;
}
/// @notice Rescues ERC20 dust (e.g. leftover WBNB/quote-token remainders
/// from swap rounding). Unlike the live Factory, this contract routinely
/// holds many different quote assets, so a generic sweep is safer than
/// per-asset withdraw functions.
function rescueERC20(address token, uint256 amount) external {
if (msg.sender != platformController) revert NotController();
IERC20(token).transfer(msg.sender, amount);
}
function withdrawFeesBNB() external {
if (msg.sender != platformController) revert NotController();
uint256 bnbBalance = address(this).balance;
if (bnbBalance == 0) revert NoBNBToWithdraw();
(bool success, ) = msg.sender.call{value: bnbBalance}("");
require(success, "BNB transfer failed");
}
// ---------------------------------------------------------------------
// Launch
// ---------------------------------------------------------------------
/// @param initialBuyQuoteAmount Optional - extra quote token (on top of
/// config.quoteAmountDesired) to spend buying the new token right after
/// launch. Both amounts are pulled together via transferFrom, so approve
/// this contract for at least config.quoteAmountDesired +
/// initialBuyQuoteAmount before calling.
function deployStockCoin(
string memory _name,
string memory _symbol,
string memory _metadata,
bytes32 saltSeed,
uint256 stockConfigId,
uint256 initialBuyQuoteAmount
) public nonReentrant returns (uint256 tokensReceived) {
if (!deployCoinEnabled) revert DeploymentDisabled();
if (stockConfigId >= stockConfigCount) revert InvalidConfigId();
require(address(conveyor) != address(0), "Conveyor not set");
StockLiquidityConfig memory config = stockConfigs[stockConfigId];
if (!config.enabled) revert DeploymentDisabled();
uint256 totalQuoteNeeded = config.quoteAmountDesired + initialBuyQuoteAmount;
IERC20(config.quoteToken).transferFrom(
msg.sender,
address(this),
totalQuoteNeeded
);
(bytes32 chosenSalt, address predicted) = conveyor
.findSaltTokenLTQuote(_name, _symbol, saltSeed, config.quoteToken);
(address coinAddress, address predicted2) = conveyor.deployToken(
_name,
_symbol,
chosenSalt,
msg.sender,
config.quoteToken
);
require(
coinAddress == predicted && predicted2 == predicted,
"Predicted mismatch"
);
tokenCreators[coinAddress] = msg.sender;
quoteTokenOf[coinAddress] = config.quoteToken;
_setupLiquidity(coinAddress, config);
tokensReceived = _handleInitialBuy(
coinAddress,
config.quoteToken,
initialBuyQuoteAmount
);
_storeTokenInfo(coinAddress, _name, _symbol, _metadata, config.quoteToken);
return tokensReceived;
}
function _setupLiquidity(
address coinAddress,
StockLiquidityConfig memory config
) internal {
address quoteToken = config.quoteToken;
if (quoteToken >= coinAddress) revert TokenMustBeToken0();
IERC20(quoteToken).approve(POSITION_MANAGER, type(uint256).max);
IERC20(coinAddress).approve(POSITION_MANAGER, type(uint256).max);
INonfungiblePositionManager manager = INonfungiblePositionManager(
POSITION_MANAGER
);
address pool = manager.createAndInitializePoolIfNecessary(
quoteToken,
coinAddress,
FEE_TIER,
config.sqrtPriceX96
);
tokenToPool[coinAddress] = pool;
(uint256 tokenId, , , ) = manager.mint(
INonfungiblePositionManager.MintParams({
token0: quoteToken,
token1: coinAddress,
fee: FEE_TIER,
tickLower: config.tickLower,
tickUpper: config.tickUpper,
amount0Desired: config.quoteAmountDesired,
amount1Desired: config.tokenAmountDesired,
amount0Min: 0,
amount1Min: 0,
recipient: address(this),
deadline: block.timestamp
})
);
tokenToNFTId[coinAddress] = tokenId;
}
function _toHex(bytes memory buffer) public pure returns (string memory) {
bytes memory converted = new bytes(buffer.length * 2);
bytes memory _base = "0123456789abcdef";
for (uint256 i = 0; i < buffer.length; i++) {
converted[i * 2] = _base[uint8(buffer[i]) / _base.length];
converted[i * 2 + 1] = _base[uint8(buffer[i]) % _base.length];
}
return string(abi.encodePacked("0x", converted));
}
/// @dev Single-hop quoteToken -> newToken swap, spending the extra
/// quote-token amount the creator supplied on top of the liquidity dust.
function _handleInitialBuy(
address coinAddress,
address quoteToken,
uint256 buyAmountQuote
) internal returns (uint256) {
if (buyAmountQuote == 0) return 0;
address expected = IUniswapV3Factory(FACTORY).getPool(
coinAddress,
quoteToken,
FEE_TIER
);
require(expected != address(0), "initial buy: pool not found for fee");
require(
expected == tokenToPool[coinAddress],
"initial buy: router/factory pool mismatch"
);
require(expected.code.length > 0, "initial buy: pool has no code");
require(
IUniswapV3PoolState(expected).liquidity() > 0,
"initial buy: pool has zero active liquidity"
);
if (
IERC20(quoteToken).allowance(address(this), SWAP_ROUTER) <
buyAmountQuote
) {
IERC20(quoteToken).approve(SWAP_ROUTER, type(uint256).max);
}
ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02
.ExactInputSingleParams({
tokenIn: quoteToken,
tokenOut: coinAddress,
fee: FEE_TIER,
recipient: address(this),
amountIn: buyAmountQuote,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
uint256 amountOut;
try ISwapRouter02(SWAP_ROUTER).exactInputSingle(params) returns (
uint256 out
) {
amountOut = out;
} catch Error(string memory reason) {
revert(string.concat("initial buy swap failed: ", reason));
} catch (bytes memory data) {
revert(
string.concat("initial buy swap failed (raw): ", _toHex(data))
);
}
IERC20(coinAddress).transfer(msg.sender, amountOut);
indexer.emitTokenPurchased(
msg.sender,
coinAddress,
buyAmountQuote,
amountOut
);
return amountOut;
}
function _storeTokenInfo(
address coinAddress,
string memory name,
string memory symbol,
string memory metadata,
address quoteToken
) internal {
TokenInfo memory newTokenInfo = TokenInfo({
tokenAddress: coinAddress,
name: name,
symbol: symbol,
deployer: msg.sender,
quoteToken: quoteToken,
time: block.timestamp,
metadata: metadata,
nftId: tokenToNFTId[coinAddress]
});
deployedTokens[tokenCount] = newTokenInfo;
tokenInfoByAddress[coinAddress] = newTokenInfo;
deployerTokens[msg.sender].push(coinAddress);
unchecked {
++tokenCount;
}
}
function getAllTokensByDeployer(
address _deployer
) public view returns (address[] memory) {
return deployerTokens[_deployer];
}
// ---------------------------------------------------------------------
// Fees - paid out in the quote token itself, not unwrapped to ETH. A
// creator pairing against TSLA earns fees in TSLA, matching the pitch:
// creator fees accrue in the stock, not gas-token dust.
// ---------------------------------------------------------------------
function collectStockFeesAndBuyback(
address tokenAddress
)
external
nonReentrant
returns (
uint256 tokenFeesCollected,
uint256 quoteFeesCollected,
uint256 creatorQuotePaid
)
{
uint256 tokenId = tokenToNFTId[tokenAddress];
if (tokenId == 0) revert NoPosition();
address quoteToken = quoteTokenOf[tokenAddress];
if (quoteToken == address(0)) revert QuoteTokenNotSet();
address pool = tokenToPool[tokenAddress];
require(pool != address(0), "No pool");
address token0 = IUniswapV3PoolTokens(pool).token0();
address token1 = IUniswapV3PoolTokens(pool).token1();
if (token0 != quoteToken || token1 != tokenAddress) {
revert PoolTokenMismatch();
}
uint256 beforeQuote = IERC20(quoteToken).balanceOf(address(this));
uint256 beforeToken = IERC20(tokenAddress).balanceOf(address(this));
INonfungiblePositionManager(POSITION_MANAGER).collect(
INonfungiblePositionManager.CollectParams({
tokenId: tokenId,
recipient: address(this),
amount0Max: type(uint128).max,
amount1Max: type(uint128).max
})
);
quoteFeesCollected =
IERC20(quoteToken).balanceOf(address(this)) -
beforeQuote;
tokenFeesCollected =
IERC20(tokenAddress).balanceOf(address(this)) -
beforeToken;
if (quoteFeesCollected > 0) {
tokenFeesGenerated[tokenAddress] += quoteFeesCollected;
tokenFeesClaimed[tokenAddress] += quoteFeesCollected;
}
// Always burn/dead-send the launched token's own fee leg, same as
// the live Factory does for its non-WBNB leg.
if (tokenFeesCollected > 0) {
_burnOrSendToDead(tokenAddress, tokenFeesCollected);
}
uint256 platformQuote = 0;
if (quoteFeesCollected > 0) {
address creator = tokenCreators[tokenAddress];
if (creator != address(0)) {
uint256 creatorAmount = (quoteFeesCollected * 20) / 100;
platformQuote = quoteFeesCollected - creatorAmount;
IERC20(quoteToken).transfer(creator, creatorAmount);
creatorQuotePaid = creatorAmount;
} else {
platformQuote = quoteFeesCollected;
}
}
if (devAddress != address(0) && platformQuote > 0) {
uint256 devAmount = (platformQuote * 50) / 100;
platformQuote -= devAmount;
IERC20(quoteToken).transfer(devAddress, devAmount);
}
if (buybackPool != address(0) && platformQuote > 0) {
IERC20(quoteToken).transfer(buybackPool, platformQuote);
}
indexer.emitFeesCollected(
tokenId,
tokenAddress,
tokenFeesCollected,
quoteFeesCollected
);
}
function _burnOrSendToDead(address token, uint256 amount) internal {
if (amount == 0) return;
try ERC20Burnable(token).burn(amount) {} catch {
IERC20(token).transfer(
0x000000000000000000000000000000000000dEaD,
amount
);
}
}
function getTokenFeesGenerated(
address tokenAddress
) public view returns (uint256) {
return tokenFeesGenerated[tokenAddress];
}
function getTokenFeesClaimed(
address tokenAddress
) public view returns (uint256) {
return tokenFeesClaimed[tokenAddress];
}
/// @notice Live market cap denominated in the token's own quote asset
/// (e.g. "in TSLA"), not USD - USD conversion happens off-chain using
/// the same stock price feed the /stocks page already reads.
function getTokenMarketCapInQuote(
address tokenAddress
) public view returns (uint256 marketCapInQuote) {
address quoteToken = quoteTokenOf[tokenAddress];
if (quoteToken == address(0)) return 0;
address pool = tokenToPool[tokenAddress];
if (pool == address(0)) return 0;
uint256 totalSupply = IERC20(tokenAddress).totalSupply();
(uint160 sqrtPriceX96, , , , , , ) = IUniswapV3Pool(pool).slot0();
if (sqrtPriceX96 == 0 || totalSupply == 0) return 0;
// quoteToken is always token0 and tokenAddress always token1 by
// construction (see findSaltTokenLTQuote), so price is quote-per-token.
uint256 priceX192 = uint256(sqrtPriceX96) * uint256(sqrtPriceX96);
marketCapInQuote = Math.mulDiv(totalSupply, priceX192, 1 << 192);
}
function getTokenStats(
address tokenAddress
)
external
view
returns (
uint256 marketCapInQuote,
uint256 totalFeesGenerated,
address creatorAddress,
address quoteToken
)
{
creatorAddress = tokenCreators[tokenAddress];
quoteToken = quoteTokenOf[tokenAddress];
marketCapInQuote = getTokenMarketCapInQuote(tokenAddress);
totalFeesGenerated = tokenFeesGenerated[tokenAddress];
}
}
Chain explorer5522msChain node89ms