From 0fd83377deedd5ce019c4541e6bd9ea40e536bc9 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Mon, 9 Mar 2026 21:35:52 +0800 Subject: [PATCH 1/8] init bondingV5 fully configurable contract with deployment scripts and test cases --- .openzeppelin/base-sepolia.json | 2074 +++++++++++++-- .openzeppelin/sepolia.json | 2235 ++++++++++++++++ contracts/launchpadv2/BondingConfig.sol | 362 +++ contracts/launchpadv2/BondingV5.sol | 718 ++++++ contracts/launchpadv2/FRouterV2.sol | 129 +- hardhat.config.js | 4 + scripts/launchpadv2/deployLaunchpadv2.ts | 4 - .../deployLaunchpadv2_ethSepolia.ts | 632 +++++ .../deployPrerequisites_ethSepolia.ts | 148 ++ scripts/launchpadv5/deployLaunchpadv5_0.ts | 194 ++ scripts/launchpadv5/deployLaunchpadv5_1.ts | 250 ++ scripts/launchpadv5/deployLaunchpadv5_2.ts | 393 +++ scripts/launchpadv5/deployLaunchpadv5_3.ts | 317 +++ scripts/launchpadv5/deployLaunchpadv5_4.ts | 228 ++ scripts/launchpadv5/e2e_test.ts | 781 ++++++ test/launchpadv5/bondingV5.js | 2274 +++++++++++++++++ 16 files changed, 10485 insertions(+), 258 deletions(-) create mode 100644 contracts/launchpadv2/BondingConfig.sol create mode 100644 contracts/launchpadv2/BondingV5.sol create mode 100644 scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts create mode 100644 scripts/launchpadv2/deployPrerequisites_ethSepolia.ts create mode 100644 scripts/launchpadv5/deployLaunchpadv5_0.ts create mode 100644 scripts/launchpadv5/deployLaunchpadv5_1.ts create mode 100644 scripts/launchpadv5/deployLaunchpadv5_2.ts create mode 100644 scripts/launchpadv5/deployLaunchpadv5_3.ts create mode 100644 scripts/launchpadv5/deployLaunchpadv5_4.ts create mode 100644 scripts/launchpadv5/e2e_test.ts create mode 100644 test/launchpadv5/bondingV5.js diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index 5ea92f2..66bc57d 100644 --- a/.openzeppelin/base-sepolia.json +++ b/.openzeppelin/base-sepolia.json @@ -735,6 +735,41 @@ { "address": "0x219a009f0eEc7953ee74347ba7820fF8Ce3AF369", "kind": "transparent" + }, + { + "address": "0x127A592a4bB2B2a2Ea049200fDdE99F94EB027c9", + "txHash": "0x98a34a9d780ffffbeb1c87a59f9660696ac66fa4c0e123d5947c3c37c91f85d3", + "kind": "transparent" + }, + { + "address": "0x73d55177b7E9E7453256BBC2d42D9C225350DD44", + "txHash": "0x2fe90da7a0f0e6ba7e037e41d6be9832ad6640ff08fe9457070793191ff09ff6", + "kind": "transparent" + }, + { + "address": "0x2DfA7Ee78b882B4632020442846eaB85DD92DBB0", + "txHash": "0xc6ab57235a53c247b7b1abb43e63146cbbefec6cb2b0e458b993a0ba3c8ba852", + "kind": "transparent" + }, + { + "address": "0xB957Ba0C1AEB932820c340cD161f2bfA4486634f", + "txHash": "0xadc89849db65e2fdaea0b18b5027087dc4249831c8f19493f8089cfa17e345a4", + "kind": "transparent" + }, + { + "address": "0x87bf274bc0e07Ea93B732C68D32755b1A3FbB894", + "txHash": "0x66d460777eb9bc0282f06ef9e8410872ae228a1002490d33eb66eccdc5f7de7d", + "kind": "transparent" + }, + { + "address": "0x7c9dE56D654e7c7ebba3dCe06D652796aD7B3A0e", + "txHash": "0xee95870f8fbb9f7bde7122d614cd9bd33527276fd555f5163f3ba85bed2a235c", + "kind": "transparent" + }, + { + "address": "0x08f745F2025532Cd602459d9973D8e978a10afD7", + "txHash": "0x4af16094f36abe115cad9818402a78899dce3f1cef42d3626907f49afe1c1899", + "kind": "transparent" } ], "impls": { @@ -44834,62 +44869,1739 @@ ], "numberOfBytes": "384" }, - "t_struct(DeployParams)41235_storage": { - "label": "struct BondingV4.DeployParams", + "t_struct(DeployParams)41235_storage": { + "label": "struct BondingV4.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(LaunchParams)41253_storage": { + "label": "struct BondingV4.LaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "teamTokenReservedSupply", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "teamTokenReservedWallet", + "type": "t_address", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(Token)41201_storage": { + "label": "struct BondingV4.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)41226_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "a926f6e7d33609ae97c513562d5165312914dfe1a4aec7e404619803dc4019a7": { + "address": "0xE0a33921635EAB911199e6F949ebe9289264cD5f", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "_feeTo", + "offset": 0, + "slot": "0", + "type": "t_address", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:24" + }, + { + "label": "factory", + "offset": 0, + "slot": "1", + "type": "t_contract(FFactoryV2)43164", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:26" + }, + { + "label": "router", + "offset": 0, + "slot": "2", + "type": "t_contract(FRouterV2)44616", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:27" + }, + { + "label": "initialSupply", + "offset": 0, + "slot": "3", + "type": "t_uint256", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:28" + }, + { + "label": "fee", + "offset": 0, + "slot": "4", + "type": "t_uint256", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:29" + }, + { + "label": "assetRate", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:31" + }, + { + "label": "gradThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:32" + }, + { + "label": "maxTx", + "offset": 0, + "slot": "7", + "type": "t_uint256", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:33" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "8", + "type": "t_address", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:34" + }, + { + "label": "_deployParams", + "offset": 0, + "slot": "9", + "type": "t_struct(DeployParams)41235_storage", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:83" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_address,t_struct(Token)41201_storage)", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:85" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "13", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:86" + }, + { + "label": "launchParams", + "offset": 0, + "slot": "14", + "type": "t_struct(LaunchParams)41253_storage", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:93" + }, + { + "label": "isProjectXLaunch", + "offset": 0, + "slot": "17", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:98" + }, + { + "label": "projectXLaunchFee", + "offset": 0, + "slot": "18", + "type": "t_uint256", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:99" + }, + { + "label": "isAcpSkillLaunch", + "offset": 0, + "slot": "19", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:102" + }, + { + "label": "acpSkillLaunchFee", + "offset": 0, + "slot": "20", + "type": "t_uint256", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:103" + }, + { + "label": "isAcpSkillLauncher", + "offset": 0, + "slot": "21", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:106" + }, + { + "label": "isXLauncher", + "offset": 0, + "slot": "22", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingV4", + "src": "contracts/launchpadv2/BondingV4.sol:109" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)206_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)2531_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(FFactoryV2)43164": { + "label": "contract FFactoryV2", + "numberOfBytes": "20" + }, + "t_contract(FRouterV2)44616": { + "label": "contract FRouterV2", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)41201_storage)": { + "label": "mapping(address => struct BondingV4.Token)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)41226_storage": { + "label": "struct BondingV4.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(DeployParams)41235_storage": { + "label": "struct BondingV4.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(LaunchParams)41253_storage": { + "label": "struct BondingV4.LaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "teamTokenReservedSupply", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "teamTokenReservedWallet", + "type": "t_address", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(Token)41201_storage": { + "label": "struct BondingV4.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)41226_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + }, + "allAddresses": [ + "0xE0a33921635EAB911199e6F949ebe9289264cD5f", + "0x9Df73b9eC74DBaA4886ef44182Ec4660b17596f5", + "0xD6EBb2B311a1ac35e22d42B1597e36eB3bc533c9", + "0x5d18AD6947B3f6DB3252E3cE814354Cf3725f4Db", + "0xB0bE8C6B6A06dA52696c1d3Ef2476fA02f0509E3", + "0xa57708FD53e904cbE226e115682665c624492299", + "0x602C3c2577E8392C65896A9cab30C96382b62F5d" + ] + }, + "f77e52b626ed6fdbb523654c916874323b2b818ee0c68a5d804465e5e2dd6af9": { + "address": "0x053386A6F604AF80f5D13b59dCF469d9DE496F7E", + "txHash": "0xcc6867e16a18e4a42b212a90369f9965d4c291c4c63e00c11605a257ada7b5be", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "_pair", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_mapping(t_address,t_address))", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:18" + }, + { + "label": "pairs", + "offset": 0, + "slot": "1", + "type": "t_array(t_address)dyn_storage", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:20" + }, + { + "label": "router", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:22" + }, + { + "label": "taxVault", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:24" + }, + { + "label": "buyTax", + "offset": 0, + "slot": "4", + "type": "t_uint256", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:25" + }, + { + "label": "sellTax", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:26" + }, + { + "label": "antiSniperBuyTaxStartValue", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:27" + }, + { + "label": "antiSniperTaxVault", + "offset": 0, + "slot": "7", + "type": "t_address", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:28" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)2531_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_address)": { + "label": "mapping(address => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_address))": { + "label": "mapping(address => mapping(address => address))", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "36d26e316ace60c7c0b5ca62a259eba8e6ec1f7ea6469691a9724daf05311d63": { + "address": "0xA32b00f611FdA2988DeF06b342344666687bAad2", + "txHash": "0xb577b52cfefb402dc675ef28b60c53b780037633b956579ecfa3f9b5da1c5fa3", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(FFactoryV2)45561", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:40" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "1", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:41" + }, + { + "label": "taxManager", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:42" + }, + { + "label": "antiSniperTaxManager", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:43" + }, + { + "label": "bondingV4", + "offset": 0, + "slot": "4", + "type": "t_contract(IBondingV4ForRouter)46080", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:46" + }, + { + "label": "bondingV5", + "offset": 0, + "slot": "5", + "type": "t_contract(IBondingV5ForRouter)46088", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:49" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "6", + "type": "t_contract(IBondingConfigForRouter)46101", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:50" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)2531_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_contract(FFactoryV2)45561": { + "label": "contract FFactoryV2", + "numberOfBytes": "20" + }, + "t_contract(IBondingConfigForRouter)46101": { + "label": "contract IBondingConfigForRouter", + "numberOfBytes": "20" + }, + "t_contract(IBondingV4ForRouter)46080": { + "label": "contract IBondingV4ForRouter", + "numberOfBytes": "20" + }, + "t_contract(IBondingV5ForRouter)46088": { + "label": "contract IBondingV5ForRouter", + "numberOfBytes": "20" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "5c02f82682cd74435875189a2130c8a7604448a37b10eb1e37148c81869ea731": { + "address": "0x73bb731Beb44355c80cf5c4E4DbAfC57c4d328d1", + "txHash": "0x02be5172cc8902c2be8d349d49ddf72918ea0837c9bc62efdb47bc661be6c33c", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "_scheduledLaunchParams", + "offset": 0, + "slot": "0", + "type": "t_struct(ScheduledLaunchParams)1346_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:38" + }, + { + "label": "teamTokenReservedWallet", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:74" + }, + { + "label": "maxAirdropPercent", + "offset": 20, + "slot": "3", + "type": "t_uint8", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:77" + }, + { + "label": "isXLauncher", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:80" + }, + { + "label": "isAcpSkillLauncher", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:81" + }, + { + "label": "bondingCurveParams", + "offset": 0, + "slot": "6", + "type": "t_struct(BondingCurveParams)1408_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:88" + }, + { + "label": "_deployParams", + "offset": 0, + "slot": "8", + "type": "t_struct(DeployParams)1495_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:144" + }, + { + "label": "initialSupply", + "offset": 0, + "slot": "11", + "type": "t_uint256", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:151" + }, + { + "label": "feeTo", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:152" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_struct(BondingCurveParams)1408_storage": { + "label": "struct BondingConfig.BondingCurveParams", + "members": [ + { + "label": "fakeInitialVirtualLiq", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "targetRealVirtual", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(DeployParams)1495_storage": { + "label": "struct BondingConfig.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ScheduledLaunchParams)1346_storage": { + "label": "struct BondingConfig.ScheduledLaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "normalLaunchFee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "acfFee", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "58f24d3ce75677d4c0672c81de6ed7b28b9afe34baac388c4d65d6bb91ea6e9e": { + "address": "0xDDdf4F1849e5172555c64bb18d3E6e4D92B5637F", + "txHash": "0xb9afe317b68b055af993a74839c63cc92cae40fe20b4b1df4fc5b4dc812ece8d", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(IFFactoryV2Minimal)1942", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:68" + }, + { + "label": "router", + "offset": 0, + "slot": "1", + "type": "t_contract(IFRouterV2Minimal)2004", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:69" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "2", + "type": "t_contract(IAgentFactoryV6Minimal)2069", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:70" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "3", + "type": "t_contract(BondingConfig)1909", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:71" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_struct(Token)1475_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:73" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "5", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:74" + }, + { + "label": "tokenLaunchParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_struct(LaunchParams)1486_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:80" + }, + { + "label": "tokenGradThreshold", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:83" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)158_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_contract(BondingConfig)1909": { + "label": "contract BondingConfig", + "numberOfBytes": "20" + }, + "t_contract(IAgentFactoryV6Minimal)2069": { + "label": "contract IAgentFactoryV6Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFFactoryV2Minimal)1942": { + "label": "contract IFFactoryV2Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFRouterV2Minimal)2004": { + "label": "contract IFRouterV2Minimal", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(LaunchParams)1486_storage)": { + "label": "mapping(address => struct BondingConfig.LaunchParams)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)1475_storage)": { + "label": "mapping(address => struct BondingConfig.Token)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)1436_storage": { + "label": "struct BondingConfig.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(LaunchParams)1486_storage": { + "label": "struct BondingConfig.LaunchParams", "members": [ { - "label": "tbaSalt", - "type": "t_bytes32", + "label": "launchMode", + "type": "t_uint8", "offset": 0, "slot": "0" }, { - "label": "tbaImplementation", - "type": "t_address", - "offset": 0, - "slot": "1" - }, - { - "label": "daoVotingPeriod", - "type": "t_uint32", - "offset": 20, - "slot": "1" + "label": "airdropPercent", + "type": "t_uint8", + "offset": 1, + "slot": "0" }, { - "label": "daoThreshold", - "type": "t_uint256", - "offset": 0, - "slot": "2" - } - ], - "numberOfBytes": "96" - }, - "t_struct(LaunchParams)41253_storage": { - "label": "struct BondingV4.LaunchParams", - "members": [ - { - "label": "startTimeDelay", - "type": "t_uint256", - "offset": 0, + "label": "needAcf", + "type": "t_bool", + "offset": 2, "slot": "0" }, { - "label": "teamTokenReservedSupply", - "type": "t_uint256", - "offset": 0, - "slot": "1" + "label": "antiSniperTaxType", + "type": "t_uint8", + "offset": 3, + "slot": "0" }, { - "label": "teamTokenReservedWallet", - "type": "t_address", - "offset": 0, - "slot": "2" + "label": "isProject60days", + "type": "t_bool", + "offset": 4, + "slot": "0" } ], - "numberOfBytes": "96" + "numberOfBytes": "32" }, - "t_struct(Token)41201_storage": { - "label": "struct BondingV4.Token", + "t_struct(Token)1475_storage": { + "label": "struct BondingConfig.Token", "members": [ { "label": "creator", @@ -44917,7 +46629,7 @@ }, { "label": "data", - "type": "t_struct(Data)41226_storage", + "type": "t_struct(Data)1436_storage", "offset": 0, "slot": "4" }, @@ -45002,10 +46714,6 @@ ], "numberOfBytes": "896" }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, "t_uint8": { "label": "uint8", "numberOfBytes": "1" @@ -45053,162 +46761,75 @@ } } }, - "a926f6e7d33609ae97c513562d5165312914dfe1a4aec7e404619803dc4019a7": { - "address": "0xE0a33921635EAB911199e6F949ebe9289264cD5f", + "0e720901e0b9c0729eef6dbb079a4850508dfe038219365f4af1eb3d448b6953": { + "address": "0x42D494C79A3438ce9C433275eb9ff95D0c390074", + "txHash": "0x08994b26dabb9700ace1442aaef089ab1fd2ffe1da66b5276d4a7d8cd8ea132b", "layout": { "solcVersion": "0.8.26", "storage": [ { - "label": "_feeTo", + "label": "factory", "offset": 0, "slot": "0", - "type": "t_address", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:24" + "type": "t_contract(IFFactoryV2Minimal)1942", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:68" }, { - "label": "factory", + "label": "router", "offset": 0, "slot": "1", - "type": "t_contract(FFactoryV2)43164", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:26" + "type": "t_contract(IFRouterV2Minimal)2004", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:69" }, { - "label": "router", + "label": "agentFactory", "offset": 0, "slot": "2", - "type": "t_contract(FRouterV2)44616", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:27" + "type": "t_contract(IAgentFactoryV6Minimal)2069", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:70" }, { - "label": "initialSupply", + "label": "bondingConfig", "offset": 0, "slot": "3", - "type": "t_uint256", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:28" + "type": "t_contract(BondingConfig)1909", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:71" }, { - "label": "fee", + "label": "tokenInfo", "offset": 0, "slot": "4", - "type": "t_uint256", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:29" + "type": "t_mapping(t_address,t_struct(Token)1475_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:73" }, { - "label": "assetRate", + "label": "tokenInfos", "offset": 0, "slot": "5", - "type": "t_uint256", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:31" + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:74" }, { - "label": "gradThreshold", + "label": "tokenLaunchParams", "offset": 0, "slot": "6", - "type": "t_uint256", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:32" + "type": "t_mapping(t_address,t_struct(LaunchParams)1486_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:80" }, { - "label": "maxTx", + "label": "tokenGradThreshold", "offset": 0, "slot": "7", - "type": "t_uint256", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:33" - }, - { - "label": "agentFactory", - "offset": 0, - "slot": "8", - "type": "t_address", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:34" - }, - { - "label": "_deployParams", - "offset": 0, - "slot": "9", - "type": "t_struct(DeployParams)41235_storage", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:83" - }, - { - "label": "tokenInfo", - "offset": 0, - "slot": "12", - "type": "t_mapping(t_address,t_struct(Token)41201_storage)", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:85" - }, - { - "label": "tokenInfos", - "offset": 0, - "slot": "13", - "type": "t_array(t_address)dyn_storage", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:86" - }, - { - "label": "launchParams", - "offset": 0, - "slot": "14", - "type": "t_struct(LaunchParams)41253_storage", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:93" - }, - { - "label": "isProjectXLaunch", - "offset": 0, - "slot": "17", - "type": "t_mapping(t_address,t_bool)", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:98" - }, - { - "label": "projectXLaunchFee", - "offset": 0, - "slot": "18", - "type": "t_uint256", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:99" - }, - { - "label": "isAcpSkillLaunch", - "offset": 0, - "slot": "19", - "type": "t_mapping(t_address,t_bool)", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:102" - }, - { - "label": "acpSkillLaunchFee", - "offset": 0, - "slot": "20", - "type": "t_uint256", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:103" - }, - { - "label": "isAcpSkillLauncher", - "offset": 0, - "slot": "21", - "type": "t_mapping(t_address,t_bool)", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:106" - }, - { - "label": "isXLauncher", - "offset": 0, - "slot": "22", - "type": "t_mapping(t_address,t_bool)", - "contract": "BondingV4", - "src": "contracts/launchpadv2/BondingV4.sol:109" + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:83" } ], "types": { @@ -45220,7 +46841,7 @@ "label": "bool", "numberOfBytes": "1" }, - "t_struct(InitializableStorage)1512_storage": { + "t_struct(InitializableStorage)73_storage": { "label": "struct Initializable.InitializableStorage", "members": [ { @@ -45238,7 +46859,7 @@ ], "numberOfBytes": "32" }, - "t_struct(OwnableStorage)206_storage": { + "t_struct(OwnableStorage)13_storage": { "label": "struct OwnableUpgradeable.OwnableStorage", "members": [ { @@ -45250,7 +46871,7 @@ ], "numberOfBytes": "32" }, - "t_struct(ReentrancyGuardStorage)2531_storage": { + "t_struct(ReentrancyGuardStorage)158_storage": { "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", "members": [ { @@ -45278,32 +46899,40 @@ "label": "uint8[]", "numberOfBytes": "32" }, - "t_bytes32": { - "label": "bytes32", - "numberOfBytes": "32" + "t_contract(BondingConfig)1909": { + "label": "contract BondingConfig", + "numberOfBytes": "20" }, - "t_contract(FFactoryV2)43164": { - "label": "contract FFactoryV2", + "t_contract(IAgentFactoryV6Minimal)2069": { + "label": "contract IAgentFactoryV6Minimal", "numberOfBytes": "20" }, - "t_contract(FRouterV2)44616": { - "label": "contract FRouterV2", + "t_contract(IFFactoryV2Minimal)1942": { + "label": "contract IFFactoryV2Minimal", "numberOfBytes": "20" }, - "t_mapping(t_address,t_bool)": { - "label": "mapping(address => bool)", + "t_contract(IFRouterV2Minimal)2004": { + "label": "contract IFRouterV2Minimal", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(LaunchParams)1486_storage)": { + "label": "mapping(address => struct BondingConfig.LaunchParams)", "numberOfBytes": "32" }, - "t_mapping(t_address,t_struct(Token)41201_storage)": { - "label": "mapping(address => struct BondingV4.Token)", + "t_mapping(t_address,t_struct(Token)1475_storage)": { + "label": "mapping(address => struct BondingConfig.Token)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", "numberOfBytes": "32" }, "t_string_storage": { "label": "string", "numberOfBytes": "32" }, - "t_struct(Data)41226_storage": { - "label": "struct BondingV4.Data", + "t_struct(Data)1436_storage": { + "label": "struct BondingConfig.Data", "members": [ { "label": "token", @@ -45380,62 +47009,44 @@ ], "numberOfBytes": "384" }, - "t_struct(DeployParams)41235_storage": { - "label": "struct BondingV4.DeployParams", + "t_struct(LaunchParams)1486_storage": { + "label": "struct BondingConfig.LaunchParams", "members": [ { - "label": "tbaSalt", - "type": "t_bytes32", + "label": "launchMode", + "type": "t_uint8", "offset": 0, "slot": "0" }, { - "label": "tbaImplementation", - "type": "t_address", - "offset": 0, - "slot": "1" - }, - { - "label": "daoVotingPeriod", - "type": "t_uint32", - "offset": 20, - "slot": "1" + "label": "airdropPercent", + "type": "t_uint8", + "offset": 1, + "slot": "0" }, { - "label": "daoThreshold", - "type": "t_uint256", - "offset": 0, - "slot": "2" - } - ], - "numberOfBytes": "96" - }, - "t_struct(LaunchParams)41253_storage": { - "label": "struct BondingV4.LaunchParams", - "members": [ - { - "label": "startTimeDelay", - "type": "t_uint256", - "offset": 0, + "label": "needAcf", + "type": "t_bool", + "offset": 2, "slot": "0" }, { - "label": "teamTokenReservedSupply", - "type": "t_uint256", - "offset": 0, - "slot": "1" + "label": "antiSniperTaxType", + "type": "t_uint8", + "offset": 3, + "slot": "0" }, { - "label": "teamTokenReservedWallet", - "type": "t_address", - "offset": 0, - "slot": "2" + "label": "isProject60days", + "type": "t_bool", + "offset": 4, + "slot": "0" } ], - "numberOfBytes": "96" + "numberOfBytes": "32" }, - "t_struct(Token)41201_storage": { - "label": "struct BondingV4.Token", + "t_struct(Token)1475_storage": { + "label": "struct BondingConfig.Token", "members": [ { "label": "creator", @@ -45463,7 +47074,7 @@ }, { "label": "data", - "type": "t_struct(Data)41226_storage", + "type": "t_struct(Data)1436_storage", "offset": 0, "slot": "4" }, @@ -45548,10 +47159,6 @@ ], "numberOfBytes": "896" }, - "t_uint32": { - "label": "uint32", - "numberOfBytes": "4" - }, "t_uint8": { "label": "uint8", "numberOfBytes": "1" @@ -45597,16 +47204,7 @@ } ] } - }, - "allAddresses": [ - "0xE0a33921635EAB911199e6F949ebe9289264cD5f", - "0x9Df73b9eC74DBaA4886ef44182Ec4660b17596f5", - "0xD6EBb2B311a1ac35e22d42B1597e36eB3bc533c9", - "0x5d18AD6947B3f6DB3252E3cE814354Cf3725f4Db", - "0xB0bE8C6B6A06dA52696c1d3Ef2476fA02f0509E3", - "0xa57708FD53e904cbE226e115682665c624492299", - "0x602C3c2577E8392C65896A9cab30C96382b62F5d" - ] + } } } } diff --git a/.openzeppelin/sepolia.json b/.openzeppelin/sepolia.json index 057d846..bae9650 100644 --- a/.openzeppelin/sepolia.json +++ b/.openzeppelin/sepolia.json @@ -10,6 +10,56 @@ "address": "0xa5D5e8BaFb0539dfFFa265fd0097E79f2682b005", "txHash": "0x6400e41d2b56c032e6431c3985c8dec1617b8f2af21038cf20b7871a5fdc172e", "kind": "transparent" + }, + { + "address": "0x3e14487cCB34d340c80f0AC47D4C6f2bA9B2a3eC", + "txHash": "0x911de5426e9bc8b35905a981b86f411cdf5083eed20df1e36d8af3016877a2cf", + "kind": "transparent" + }, + { + "address": "0x199CC3F130cd0afD6A53464EC257F9A854C6d0Cf", + "txHash": "0x23e09482f7ac8a0424a8b85bccad54f661191d23cd61a2c117fce7e59126bebf", + "kind": "transparent" + }, + { + "address": "0x49FF66E5DC68317D21e043e553ee56529dBce2c4", + "txHash": "0xa3a05e5ca467a495e530e5ae9dbc7103cf9da02228a68dad7d594999b410ddfc", + "kind": "transparent" + }, + { + "address": "0xdB2256261541Cf9BD23ad73fFF63Bfc8F73555b5", + "txHash": "0x02dbb6859d7e74d1e478b0d3af5bbd4cbeeb73dc193e1b2f285dcd4e676b3a5e", + "kind": "transparent" + }, + { + "address": "0x3bb3df8d17DeD5E2860A1beF6a2F89B9BF8f0D69", + "txHash": "0x161472508b9bd7e49ffc2900d0861e9090e3893d23947e00eeb4b27177dd7a4f", + "kind": "transparent" + }, + { + "address": "0xbbBa88ec1221BfaA5cc7Fe1F8aef518e5c2A2F34", + "txHash": "0x3724acc2538229b131acaabe62c9bd9a1af011a4dc609b893f43a1f14df81eda", + "kind": "transparent" + }, + { + "address": "0x4FE7efC3D1b8700E211879Da7372D3cdDD7d7965", + "txHash": "0x64fd702a38ce9b51ce0ed0f8fda9fbd989beffda16b0dceaa63af65276df844f", + "kind": "transparent" + }, + { + "address": "0x3a2C8a89eC0290a145160d696aF2Fd07A76aeD32", + "txHash": "0xb993cb2175688dc81cf997737ece01d91423984050c16aa211aa85ba1408d69b", + "kind": "transparent" + }, + { + "address": "0x56D70350c13A71c0165cb1461Eb34011F16EcBc5", + "txHash": "0xba83c87b600426d0dd0e1e95e089246b05fafe8a36d3d0a06a2e3d5d3a869b4a", + "kind": "transparent" + }, + { + "address": "0x06dcB8126e3E060c0272A5cDBd1a00Df4843f42e", + "txHash": "0x30fb8ca12ed869334caac318274fd01012b1c57b370251704574f9099169ce9b", + "kind": "transparent" } ], "impls": { @@ -432,6 +482,2191 @@ ] } } + }, + "f77e52b626ed6fdbb523654c916874323b2b818ee0c68a5d804465e5e2dd6af9": { + "address": "0x5225a1C97BE7DdbCace6fb183683a5Fc6B872E75", + "txHash": "0xd62407963ce2eed853c9347914698aa9f428f9e526065874444e89e90e213e04", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "_pair", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_address,t_mapping(t_address,t_address))", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:18" + }, + { + "label": "pairs", + "offset": 0, + "slot": "1", + "type": "t_array(t_address)dyn_storage", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:20" + }, + { + "label": "router", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:22" + }, + { + "label": "taxVault", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:24" + }, + { + "label": "buyTax", + "offset": 0, + "slot": "4", + "type": "t_uint256", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:25" + }, + { + "label": "sellTax", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:26" + }, + { + "label": "antiSniperBuyTaxStartValue", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:27" + }, + { + "label": "antiSniperTaxVault", + "offset": 0, + "slot": "7", + "type": "t_address", + "contract": "FFactoryV2", + "src": "contracts/launchpadv2/FFactoryV2.sol:28" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)211_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)296_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_address)": { + "label": "mapping(address => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_address))": { + "label": "mapping(address => mapping(address => address))", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "cc50fb5dafb0312095ede1587a42c2bb070b282061425e49994166065a4a4b9a": { + "address": "0xCEC5F7F75413390a4651Ee504fB8948374debb95", + "txHash": "0x445682b71d79a85d44c9f192c5b617f87a6a3087674f86a6b0bfb191e7cadb2f", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(FFactoryV2)4382", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:29" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "1", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:30" + }, + { + "label": "taxManager", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:31" + }, + { + "label": "antiSniperTaxManager", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:32" + }, + { + "label": "bondingV4", + "offset": 0, + "slot": "4", + "type": "t_contract(IBondingV4ForRouter)4901", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:35" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)211_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)296_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_contract(FFactoryV2)4382": { + "label": "contract FFactoryV2", + "numberOfBytes": "20" + }, + "t_contract(IBondingV4ForRouter)4901": { + "label": "contract IBondingV4ForRouter", + "numberOfBytes": "20" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "eb9add12361657ca96e1cf814bcfc055a61175c94aa2cda7800f234d853f6e3c": { + "address": "0xb207a142FB04E9740F2C5c0D1fF5891308114839", + "txHash": "0xec3d4be07759a8f0a717029f68e5bc32a9ad7edd8793464ed9d595721b35b36a", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "_roles", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_struct(RoleData)7982_storage)", + "contract": "AccessControl", + "src": "@openzeppelin/contracts/access/AccessControl.sol:55" + }, + { + "label": "_nextId", + "offset": 0, + "slot": "1", + "type": "t_uint256", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:27" + }, + { + "label": "tokenImplementation", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:30" + }, + { + "label": "daoImplementation", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:31" + }, + { + "label": "nft", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:32" + }, + { + "label": "tbaRegistry", + "offset": 0, + "slot": "5", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:33" + }, + { + "label": "allTokens", + "offset": 0, + "slot": "6", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:35" + }, + { + "label": "allDAOs", + "offset": 0, + "slot": "7", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:36" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "8", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:38" + }, + { + "label": "maturityDuration", + "offset": 0, + "slot": "9", + "type": "t_uint256", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:39" + }, + { + "label": "_applications", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_uint256,t_struct(Application)63935_storage)", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:76" + }, + { + "label": "gov", + "offset": 0, + "slot": "11", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:78" + }, + { + "label": "_vault", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:89" + }, + { + "label": "locked", + "offset": 20, + "slot": "12", + "type": "t_bool", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:91" + }, + { + "label": "allTradingTokens", + "offset": 0, + "slot": "13", + "type": "t_array(t_address)dyn_storage", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:103" + }, + { + "label": "_uniswapRouter", + "offset": 0, + "slot": "14", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:104" + }, + { + "label": "veTokenImplementation", + "offset": 0, + "slot": "15", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:105" + }, + { + "label": "_minter", + "offset": 0, + "slot": "16", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:106" + }, + { + "label": "_tokenAdmin", + "offset": 0, + "slot": "17", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:107" + }, + { + "label": "defaultDelegatee", + "offset": 0, + "slot": "18", + "type": "t_address", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:108" + }, + { + "label": "_tokenSupplyParams", + "offset": 0, + "slot": "19", + "type": "t_bytes_storage", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:111" + }, + { + "label": "_tokenTaxParams", + "offset": 0, + "slot": "20", + "type": "t_bytes_storage", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:112" + }, + { + "label": "_tokenMultiplier", + "offset": 0, + "slot": "21", + "type": "t_uint16", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:113" + }, + { + "label": "_existingAgents", + "offset": 0, + "slot": "22", + "type": "t_mapping(t_address,t_bool)", + "contract": "AgentFactoryV6", + "src": "contracts/virtualPersona/AgentFactoryV6.sol:121" + } + ], + "types": { + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(PausableStorage)2467_storage": { + "label": "struct PausableUpgradeable.PausableStorage", + "members": [ + { + "label": "_paused", + "type": "t_bool", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_bytes_storage": { + "label": "bytes", + "numberOfBytes": "32" + }, + "t_enum(ApplicationStatus)63904": { + "label": "enum AgentFactoryV6.ApplicationStatus", + "members": [ + "Active", + "Executed", + "Withdrawn" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)7982_storage)": { + "label": "mapping(bytes32 => struct AccessControl.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(Application)63935_storage)": { + "label": "mapping(uint256 => struct AgentFactoryV6.Application)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Application)63935_storage": { + "label": "struct AgentFactoryV6.Application", + "members": [ + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "tokenURI", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "status", + "type": "t_enum(ApplicationStatus)63904", + "offset": 0, + "slot": "3" + }, + { + "label": "withdrawableAmount", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "proposer", + "type": "t_address", + "offset": 0, + "slot": "5" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "6" + }, + { + "label": "proposalEndBlock", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "9" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "10" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "10" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "11" + }, + { + "label": "tokenAddress", + "type": "t_address", + "offset": 0, + "slot": "12" + } + ], + "numberOfBytes": "416" + }, + "t_struct(RoleData)7982_storage": { + "label": "struct AccessControl.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Pausable": [ + { + "contract": "PausableUpgradeable", + "label": "_paused", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol:21", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "157fc4c2e85f80c9495cce9af8f1e5ec2860fd03e6ef17cd4a2c0b11c6f9bdcc": { + "address": "0x05Bd92d37b74a361757C848F85C9B29cE1B6137b", + "txHash": "0x617d347e48881f3212857e775c6dffbd230f8f77baba62c8c3629d1ec871df95", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "_feeTo", + "offset": 0, + "slot": "0", + "type": "t_address", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:24" + }, + { + "label": "factory", + "offset": 0, + "slot": "1", + "type": "t_contract(FFactoryV2)43164", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:26" + }, + { + "label": "router", + "offset": 0, + "slot": "2", + "type": "t_contract(FRouterV2)44616", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:27" + }, + { + "label": "initialSupply", + "offset": 0, + "slot": "3", + "type": "t_uint256", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:28" + }, + { + "label": "fee", + "offset": 0, + "slot": "4", + "type": "t_uint256", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:29" + }, + { + "label": "assetRate", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:31" + }, + { + "label": "gradThreshold", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:32" + }, + { + "label": "maxTx", + "offset": 0, + "slot": "7", + "type": "t_uint256", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:33" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "8", + "type": "t_address", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:34" + }, + { + "label": "_deployParams", + "offset": 0, + "slot": "9", + "type": "t_struct(DeployParams)37993_storage", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:83" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_address,t_struct(Token)37959_storage)", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:85" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "13", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:86" + }, + { + "label": "launchParams", + "offset": 0, + "slot": "14", + "type": "t_struct(LaunchParams)38011_storage", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:93" + }, + { + "label": "isProject60days", + "offset": 0, + "slot": "17", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:98" + }, + { + "label": "project60daysLaunchFee", + "offset": 0, + "slot": "18", + "type": "t_uint256", + "contract": "BondingV2", + "src": "contracts/launchpadv2/BondingV2.sol:99" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)206_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)2531_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_contract(FFactoryV2)43164": { + "label": "contract FFactoryV2", + "numberOfBytes": "20" + }, + "t_contract(FRouterV2)44616": { + "label": "contract FRouterV2", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)37959_storage)": { + "label": "mapping(address => struct BondingV2.Token)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)37984_storage": { + "label": "struct BondingV2.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(DeployParams)37993_storage": { + "label": "struct BondingV2.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(LaunchParams)38011_storage": { + "label": "struct BondingV2.LaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "teamTokenReservedSupply", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "teamTokenReservedWallet", + "type": "t_address", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(Token)37959_storage": { + "label": "struct BondingV2.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)37984_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "4404c04f459a91d19124ebe6b6ddae906577fc2e37f77bf4f411cd18a8a65cd8": { + "address": "0x335b21FCc1eed4EF2E5BF2DbCb1D97f484cA9338", + "txHash": "0xc59c7ad0db2d76dd8fcbb5fe31982e48d4de208b4e41e92f88768ad841e8abb7", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "coreTypes", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_uint8,t_string_storage)", + "contract": "CoreRegistry", + "src": "contracts/virtualPersona/CoreRegistry.sol:7" + }, + { + "label": "_nextCoreType", + "offset": 0, + "slot": "1", + "type": "t_uint8", + "contract": "CoreRegistry", + "src": "contracts/virtualPersona/CoreRegistry.sol:8" + }, + { + "label": "_validatorsMap", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_mapping(t_address,t_bool))", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:8" + }, + { + "label": "_baseValidatorScore", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_address,t_mapping(t_uint256,t_uint256))", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:10" + }, + { + "label": "_validators", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_array(t_address)dyn_storage)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:12" + }, + { + "label": "_getScoreOf", + "offset": 0, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:14" + }, + { + "label": "_getMaxScore", + "offset": 8, + "slot": "5", + "type": "t_function_internal_view(t_uint256)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:15" + }, + { + "label": "_getPastScore", + "offset": 16, + "slot": "5", + "type": "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)", + "contract": "ValidatorRegistry", + "src": "contracts/virtualPersona/ValidatorRegistry.sol:16" + }, + { + "label": "_nextVirtualId", + "offset": 0, + "slot": "6", + "type": "t_uint256", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:27" + }, + { + "label": "_stakingTokenToVirtualId", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:28" + }, + { + "label": "virtualInfos", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_uint256,t_struct(VirtualInfo)72579_storage)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:47" + }, + { + "label": "_contributionNft", + "offset": 0, + "slot": "9", + "type": "t_address", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:49" + }, + { + "label": "_serviceNft", + "offset": 0, + "slot": "10", + "type": "t_address", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:50" + }, + { + "label": "_blacklists", + "offset": 0, + "slot": "11", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:54" + }, + { + "label": "virtualLPs", + "offset": 0, + "slot": "12", + "type": "t_mapping(t_uint256,t_struct(VirtualLP)72591_storage)", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:55" + }, + { + "label": "_eloCalculator", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "AgentNftV2", + "src": "contracts/virtualPersona/AgentNftV2.sol:56" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_mapping(t_address,t_bool))": { + "label": "mapping(address => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_string_storage)": { + "label": "mapping(uint256 => string)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ERC721Storage)1869_storage": { + "label": "struct ERC721Upgradeable.ERC721Storage", + "members": [ + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "0" + }, + { + "label": "_symbol", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "2" + }, + { + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "offset": 0, + "slot": "3" + }, + { + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "offset": 0, + "slot": "4" + }, + { + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "offset": 0, + "slot": "5" + } + ], + "numberOfBytes": "192" + }, + "t_struct(ERC721URIStorageStorage)2327_storage": { + "label": "struct ERC721URIStorageUpgradeable.ERC721URIStorageStorage", + "members": [ + { + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_function_internal_view(t_uint256)returns(t_uint256)": { + "label": "function (uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address)returns(t_uint256)": { + "label": "function (uint256,address) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_function_internal_view(t_uint256,t_address,t_uint256)returns(t_uint256)": { + "label": "function (uint256,address,uint256) view returns (uint256)", + "numberOfBytes": "8" + }, + "t_mapping(t_address,t_mapping(t_uint256,t_uint256))": { + "label": "mapping(address => mapping(uint256 => uint256))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_array(t_address)dyn_storage)": { + "label": "mapping(uint256 => address[])", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_mapping(t_address,t_bool))": { + "label": "mapping(uint256 => mapping(address => bool))", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(VirtualInfo)72579_storage)": { + "label": "mapping(uint256 => struct IAgentNft.VirtualInfo)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(VirtualLP)72591_storage)": { + "label": "mapping(uint256 => struct IAgentNft.VirtualLP)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_uint256)": { + "label": "mapping(uint256 => uint256)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint8,t_string_storage)": { + "label": "mapping(uint8 => string)", + "numberOfBytes": "32" + }, + "t_struct(VirtualInfo)72579_storage": { + "label": "struct IAgentNft.VirtualInfo", + "members": [ + { + "label": "dao", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "founder", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "tba", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "coreTypes", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "4" + } + ], + "numberOfBytes": "160" + }, + "t_struct(VirtualLP)72591_storage": { + "label": "struct IAgentNft.VirtualLP", + "members": [ + { + "label": "pool", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "veToken", + "type": "t_address", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721URIStorage": [ + { + "contract": "ERC721URIStorageUpgradeable", + "label": "_tokenURIs", + "type": "t_mapping(t_uint256,t_string_storage)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol:25", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ERC721": [ + { + "contract": "ERC721Upgradeable", + "label": "_name", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:27", + "offset": 0, + "slot": "0" + }, + { + "contract": "ERC721Upgradeable", + "label": "_symbol", + "type": "t_string_storage", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:30", + "offset": 0, + "slot": "1" + }, + { + "contract": "ERC721Upgradeable", + "label": "_owners", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:32", + "offset": 0, + "slot": "2" + }, + { + "contract": "ERC721Upgradeable", + "label": "_balances", + "type": "t_mapping(t_address,t_uint256)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:34", + "offset": 0, + "slot": "3" + }, + { + "contract": "ERC721Upgradeable", + "label": "_tokenApprovals", + "type": "t_mapping(t_uint256,t_address)", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:36", + "offset": 0, + "slot": "4" + }, + { + "contract": "ERC721Upgradeable", + "label": "_operatorApprovals", + "type": "t_mapping(t_address,t_mapping(t_address,t_bool))", + "src": "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol:38", + "offset": 0, + "slot": "5" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "fc23196267056d6141e5d34afb2d32202d9daa3bfd67324258cc7d9a43ef1a4c": { + "address": "0xa5Ea471FC01a65bcBE1a757c82a5DC1eaAe0e46a", + "txHash": "0xe83fb21c228cc11053eebe987be2d0d95d574dbb37bb88a72beea2b9e12c81a2", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "assetToken", + "offset": 0, + "slot": "0", + "type": "t_address", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:43" + }, + { + "label": "taxToken", + "offset": 0, + "slot": "1", + "type": "t_address", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:44" + }, + { + "label": "router", + "offset": 0, + "slot": "2", + "type": "t_contract(IRouter)51611", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:45" + }, + { + "label": "treasury", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:46" + }, + { + "label": "feeRate", + "offset": 20, + "slot": "3", + "type": "t_uint16", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:47" + }, + { + "label": "minSwapThreshold", + "offset": 0, + "slot": "4", + "type": "t_uint256", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:48" + }, + { + "label": "maxSwapThreshold", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:49" + }, + { + "label": "agentNft", + "offset": 0, + "slot": "6", + "type": "t_contract(IAgentNft)72707", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:50" + }, + { + "label": "_agentTba", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_uint256,t_address)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:67" + }, + { + "label": "taxHistory", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_bytes32,t_struct(TaxHistory)53360_storage)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:68" + }, + { + "label": "agentTaxAmounts", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_uint256,t_struct(TaxAmounts)53365_storage)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:69" + }, + { + "label": "_agentRecipients", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_uint256,t_struct(TaxRecipient)53467_storage)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:88" + }, + { + "label": "creatorFeeRate", + "offset": 0, + "slot": "11", + "type": "t_uint16", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:89" + }, + { + "label": "tbaBonus", + "offset": 2, + "slot": "11", + "type": "t_contract(ITBABonus)55297", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:97" + }, + { + "label": "bondingV2", + "offset": 0, + "slot": "12", + "type": "t_contract(IBondingV2ForTax)53332", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:98" + }, + { + "label": "bondingV4", + "offset": 0, + "slot": "13", + "type": "t_contract(IBondingV4ForTax)53347", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:99" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_contract(IAgentNft)72707": { + "label": "contract IAgentNft", + "numberOfBytes": "20" + }, + "t_contract(IBondingV2ForTax)53332": { + "label": "contract IBondingV2ForTax", + "numberOfBytes": "20" + }, + "t_contract(IBondingV4ForTax)53347": { + "label": "contract IBondingV4ForTax", + "numberOfBytes": "20" + }, + "t_contract(IRouter)51611": { + "label": "contract IRouter", + "numberOfBytes": "20" + }, + "t_contract(ITBABonus)55297": { + "label": "contract ITBABonus", + "numberOfBytes": "20" + }, + "t_mapping(t_bytes32,t_struct(TaxHistory)53360_storage)": { + "label": "mapping(bytes32 => struct AgentTax.TaxHistory)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(TaxAmounts)53365_storage)": { + "label": "mapping(uint256 => struct AgentTax.TaxAmounts)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(TaxRecipient)53467_storage)": { + "label": "mapping(uint256 => struct AgentTax.TaxRecipient)", + "numberOfBytes": "32" + }, + "t_struct(TaxAmounts)53365_storage": { + "label": "struct AgentTax.TaxAmounts", + "members": [ + { + "label": "amountCollected", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountSwapped", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TaxHistory)53360_storage": { + "label": "struct AgentTax.TaxHistory", + "members": [ + { + "label": "agentId", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TaxRecipient)53467_storage": { + "label": "struct AgentTax.TaxRecipient", + "members": [ + { + "label": "tba", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/contracts/launchpadv2/BondingConfig.sol b/contracts/launchpadv2/BondingConfig.sol new file mode 100644 index 0000000..e72eeeb --- /dev/null +++ b/contracts/launchpadv2/BondingConfig.sol @@ -0,0 +1,362 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +/** + * @title BondingConfig + * @notice Configuration contract for BondingV5 multi-chain launch modes + * @dev Stores configurable parameters for different launch modes. + * gradThreshold is calculated per-token based on airdropPercent and needAcf. + * project60days is handled by backend (not a separate launch mode). + */ +contract BondingConfig is Initializable, OwnableUpgradeable { + // Launch mode constants (only 3 modes) + uint8 public constant LAUNCH_MODE_NORMAL = 0; + uint8 public constant LAUNCH_MODE_X_LAUNCH = 1; + uint8 public constant LAUNCH_MODE_ACP_SKILL = 2; + + // Reserve percentage constants + uint8 public constant MAX_TOTAL_RESERVED_PERCENT = 55; // At least 45% must remain in bonding curve + uint8 public constant ACF_RESERVED_PERCENT = 50; // ACF operations reserve 50% + + // Anti-sniper tax type constants + // These define the duration over which anti-sniper tax decreases from 99% to 0% + uint8 public constant ANTI_SNIPER_NONE = 0; // No anti-sniper tax (0 seconds) + uint8 public constant ANTI_SNIPER_60S = 1; // 60 seconds duration (default) + uint8 public constant ANTI_SNIPER_98M = 2; // 98 minutes duration + + // Scheduled launch parameters + struct ScheduledLaunchParams { + uint256 startTimeDelay; // Time delay for scheduled launches (e.g., 24 hours) + uint256 normalLaunchFee; // Fee for scheduled launches / marketing (e.g., 100 VIRTUAL) + uint256 acfFee; // Extra fee when needAcf = true (e.g., 10 VIRTUAL on base, 150 VIRTUAL on eth) + } + // Note: Using internal + view function because Solidity auto-getter for structs + // returns individual fields, not the whole struct as memory + ScheduledLaunchParams internal _scheduledLaunchParams; + + function scheduledLaunchParams() + external + view + returns (ScheduledLaunchParams memory) + { + return _scheduledLaunchParams; + } + + /** + * @notice Calculate total launch fee based on launch type and ACF requirement + * @dev Fee structure: + * - Immediate launch, no ACF: 0 + * - Immediate launch, with ACF: acfFee + * - Scheduled launch, no ACF: normalLaunchFee + * - Scheduled launch, with ACF: normalLaunchFee + acfFee + * @param isScheduledLaunch_ Whether this is a scheduled launch + * @param needAcf_ Whether ACF operations are needed + * @return totalFee The total fee to charge + */ + function calculateLaunchFee( + bool isScheduledLaunch_, + bool needAcf_ + ) external view returns (uint256) { + uint256 totalFee = 0; + if (isScheduledLaunch_) { + totalFee += _scheduledLaunchParams.normalLaunchFee; + } + if (needAcf_) { + totalFee += _scheduledLaunchParams.acfFee; + } + return totalFee; + } + + // Global wallet to receive reserved tokens (airdrop + ACF) + address public teamTokenReservedWallet; + + // Maximum airdrop percentage allowed (e.g., 5%) + uint8 public maxAirdropPercent; + + // Authorized launchers for special modes + mapping(address => bool) public isXLauncher; + mapping(address => bool) public isAcpSkillLauncher; + + // Common bonding curve parameters (unified across all launch modes) + struct BondingCurveParams { + uint256 fakeInitialVirtualLiq; // Fixed fake initial VIRTUAL liquidity (e.g., 6300 * 1e18) + uint256 targetRealVirtual; // Target VIRTUAL from users at graduation (e.g., 42000 * 1e18) + } + BondingCurveParams public bondingCurveParams; + + struct Data { + address token; + string name; + string _name; + string ticker; + uint256 supply; + uint256 price; + uint256 marketCap; + uint256 liquidity; + uint256 volume; + uint256 volume24H; + uint256 prevPrice; + uint256 lastUpdated; + } + + struct Token { + address creator; + address token; + address pair; + address agentToken; + Data data; + string description; + uint8[] cores; + string image; + string twitter; + string telegram; + string youtube; + string website; + bool trading; + bool tradingOnUniswap; + uint256 applicationId; + uint256 initialPurchase; + uint256 virtualId; + bool launchExecuted; + } + + // V5 configurable launch parameters (stored separately per token) + struct LaunchParams { + uint8 launchMode; + uint8 airdropPercent; + bool needAcf; + uint8 antiSniperTaxType; + bool isProject60days; + } + + // Common deploy parameters shared across all modes + struct DeployParams { + bytes32 tbaSalt; + address tbaImplementation; + uint32 daoVotingPeriod; + uint256 daoThreshold; + } + // Note: Using internal + view function because Solidity auto-getter for structs + // returns individual fields, not the whole struct as memory + DeployParams internal _deployParams; + + function deployParams() external view returns (DeployParams memory) { + return _deployParams; + } + + // Common parameters + uint256 public initialSupply; + address public feeTo; + + event DeployParamsUpdated(DeployParams params); + event TeamTokenReservedWalletUpdated(address wallet); + event CommonParamsUpdated(uint256 initialSupply, address feeTo); + event BondingCurveParamsUpdated(BondingCurveParams params); + event MaxAirdropPercentUpdated(uint8 maxAirdropPercent); + + error InvalidAntiSniperType(); + error InvalidReservePercent(); + error AirdropPercentExceedsMax(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + uint256 initialSupply_, + address feeTo_, + address teamTokenReservedWallet_, + uint8 maxAirdropPercent_, + ScheduledLaunchParams memory scheduledLaunchParams_, + DeployParams memory deployParams_, + BondingCurveParams memory bondingCurveParams_ + ) external initializer { + __Ownable_init(msg.sender); + + initialSupply = initialSupply_; + feeTo = feeTo_; + teamTokenReservedWallet = teamTokenReservedWallet_; + maxAirdropPercent = maxAirdropPercent_; + _scheduledLaunchParams = scheduledLaunchParams_; + _deployParams = deployParams_; + bondingCurveParams = bondingCurveParams_; + } + + /** + * @notice Set deploy parameters + * @param params_ The deploy parameters + */ + function setDeployParams(DeployParams memory params_) external onlyOwner { + _deployParams = params_; + emit DeployParamsUpdated(params_); + } + + /** + * @notice Set common parameters + * @param initialSupply_ Initial token supply + * @param feeTo_ Address to receive fees + */ + function setCommonParams( + uint256 initialSupply_, + address feeTo_ + ) external onlyOwner { + initialSupply = initialSupply_; + feeTo = feeTo_; + emit CommonParamsUpdated(initialSupply_, feeTo_); + } + + /** + * @notice Set bonding curve parameters + * @param params_ The bonding curve parameters (K, assetRate, targetRealVirtual) + */ + function setBondingCurveParams( + BondingCurveParams memory params_ + ) external onlyOwner { + bondingCurveParams = params_; + emit BondingCurveParamsUpdated(params_); + } + + /** + * @notice Calculate bonding curve supply with validation + * @dev Validates: + * 1. airdropPercent_ <= maxAirdropPercent + * 2. airdropPercent + (needAcf ? 50 : 0) < MAX_TOTAL_RESERVED_PERCENT + * @param airdropPercent_ Airdrop percentage + * @param needAcf_ Whether ACF operations are needed (adds 50% reserve) + * @return bondingCurveSupply The supply available for bonding curve (in base units, not wei) + */ + function calculateBondingCurveSupply( + uint8 airdropPercent_, + bool needAcf_ + ) external view returns (uint256) { + if (airdropPercent_ > maxAirdropPercent) { + revert AirdropPercentExceedsMax(); + } + uint8 totalReserved = airdropPercent_ + + (needAcf_ ? ACF_RESERVED_PERCENT : 0); + if (totalReserved >= MAX_TOTAL_RESERVED_PERCENT) { + revert InvalidReservePercent(); + } + return (initialSupply * (100 - totalReserved)) / 100; + } + + /** + * @notice Get the fixed fake initial virtual liquidity + * @return fakeInitialVirtualLiq The fake initial VIRTUAL liquidity in wei + */ + function getFakeInitialVirtualLiq() external view returns (uint256) { + return bondingCurveParams.fakeInitialVirtualLiq; + } + + /** + * @notice Calculate graduation threshold for a token + * @dev Formula: gradThreshold = fakeInitialVirtualLiq * bondingCurveSupply / (targetRealVirtual + fakeInitialVirtualLiq) + * @param bondingCurveSupplyWei_ Bonding curve supply in wei + * @return gradThreshold The graduation threshold (agent token amount in wei) + */ + function calculateGradThreshold( + uint256 bondingCurveSupplyWei_ + ) external view returns (uint256) { + // gradThreshold = y0 * x0 / (targetRealVirtual + y0) + // where y0 = fakeInitialVirtualLiq (fixed), x0 = bondingCurveSupply + uint256 fakeInitialVirtualLiq = bondingCurveParams + .fakeInitialVirtualLiq; + return + (fakeInitialVirtualLiq * bondingCurveSupplyWei_) / + (bondingCurveParams.targetRealVirtual + fakeInitialVirtualLiq); + } + + /** + * @notice Update scheduled launch parameters + * @param params_ The new scheduled launch parameters (startTimeDelay, fee) + */ + function setScheduledLaunchParams( + ScheduledLaunchParams memory params_ + ) external onlyOwner { + _scheduledLaunchParams = params_; + } + + /** + * @notice Update team token reserved wallet + * @param wallet_ The team token reserved wallet + */ + function setTeamTokenReservedWallet(address wallet_) external onlyOwner { + teamTokenReservedWallet = wallet_; + emit TeamTokenReservedWalletUpdated(wallet_); + } + + function setXLauncher(address launcher_, bool allowed_) external onlyOwner { + isXLauncher[launcher_] = allowed_; + } + + function setAcpSkillLauncher( + address launcher_, + bool allowed_ + ) external onlyOwner { + isAcpSkillLauncher[launcher_] = allowed_; + } + + /** + * @notice Get target real VIRTUAL value + * @return targetRealVirtual value from bondingCurveParams + */ + function getTargetRealVirtual() external view returns (uint256) { + return bondingCurveParams.targetRealVirtual; + } + + /** + * @notice Check if launch mode is a special mode requiring authorized launcher + * @param launchMode_ The launch mode identifier + * @return Whether it's a special launch mode (X_LAUNCH or ACP_SKILL) + */ + function isSpecialMode(uint8 launchMode_) external pure returns (bool) { + return + launchMode_ == LAUNCH_MODE_X_LAUNCH || + launchMode_ == LAUNCH_MODE_ACP_SKILL; + } + + /** + * @notice Set maximum airdrop percentage + * @param maxAirdropPercent_ The maximum airdrop percentage (e.g., 5 for 5%) + */ + function setMaxAirdropPercent(uint8 maxAirdropPercent_) external onlyOwner { + maxAirdropPercent = maxAirdropPercent_; + emit MaxAirdropPercentUpdated(maxAirdropPercent_); + } + + /** + * @notice Check if anti-sniper tax type is valid + * @param antiSniperType_ The anti-sniper type (0=none, 1=60s, 2=98min) + * @return Whether the type is valid + */ + function isValidAntiSniperType( + uint8 antiSniperType_ + ) external pure returns (bool) { + return + antiSniperType_ == ANTI_SNIPER_NONE || + antiSniperType_ == ANTI_SNIPER_60S || + antiSniperType_ == ANTI_SNIPER_98M; + } + + /** + * @notice Get anti-sniper tax duration in seconds for a given type + * @param antiSniperType_ The anti-sniper type + * @return Duration in seconds (0 for NONE, 60 for 60S, 5880 for 98M) + */ + function getAntiSniperDuration( + uint8 antiSniperType_ + ) external pure returns (uint256) { + if (antiSniperType_ == ANTI_SNIPER_NONE) { + return 0; + } else if (antiSniperType_ == ANTI_SNIPER_60S) { + return 60; // 60 seconds + } else if (antiSniperType_ == ANTI_SNIPER_98M) { + return 5880; // 98 minutes = 98 * 60 = 5880 seconds + } + revert InvalidAntiSniperType(); + } +} diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol new file mode 100644 index 0000000..27792bb --- /dev/null +++ b/contracts/launchpadv2/BondingV5.sol @@ -0,0 +1,718 @@ +// SPDX-License-Identifier: MIT +// Modified from https://github.com/sourlodine/Pump.fun-Smart-Contract/blob/main/contracts/PumpFun.sol +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; + +import "./BondingConfig.sol"; +import "./IFPairV2.sol"; +import "../virtualPersona/IAgentTokenV2.sol"; + +// Minimal interfaces to reduce contract size (matches FFactoryV2/FRouterV2) +interface IFFactoryV2Minimal { + function createPair( + address tokenA, + address tokenB, + uint256 startTime, + uint256 startTimeDelay + ) external returns (address pair); + function getPair(address tokenA, address tokenB) external view returns (address pair); +} + +interface IFRouterV2Minimal { + function assetToken() external view returns (address); + function addInitialLiquidity(address token, uint256 amountToken, uint256 amountAsset) external; + function buy(uint256 amountIn, address tokenAddress, address to, bool isInitialPurchase) external returns (uint256, uint256); + function sell(uint256 amountIn, address tokenAddress, address to) external returns (uint256, uint256); + function graduate(address tokenAddress) external; + function setTaxStartTime(address pairAddress, uint256 taxStartTime) external; + function hasAntiSniperTax(address pairAddress) external view returns (bool); +} + +interface IAgentFactoryV6Minimal { + function createNewAgentTokenAndApplication( + string memory name, + string memory symbol, + bytes memory tokenSupplyParams, + uint8[] memory cores, + bytes32 tbaSalt, + address tbaImplementation, + uint32 daoVotingPeriod, + uint256 daoThreshold, + uint256 applicationThreshold, + address creator + ) external returns (address, uint256); + function addBlacklistAddress(address token, address addr) external; + function removeBlacklistAddress(address token, address addr) external; + function updateApplicationThresholdWithApplicationId(uint256 applicationId, uint256 applicationThreshold) external; + function executeBondingCurveApplicationSalt( + uint256 id, + uint256 totalSupply, + uint256 lpSupply, + address vault, + bytes32 salt + ) external returns (address); +} + +contract BondingV5 is + Initializable, + ReentrancyGuardUpgradeable, + OwnableUpgradeable +{ + using SafeERC20 for IERC20; + + IFFactoryV2Minimal public factory; + IFRouterV2Minimal public router; + IAgentFactoryV6Minimal public agentFactory; + BondingConfig public bondingConfig; // Configuration contract for multi-chain launch modes + + mapping(address => BondingConfig.Token) public tokenInfo; + address[] public tokenInfos; + + // this is for BE to separate with old virtualId from bondingV1, but this field is not used yet + uint256 public constant VirtualIdBase = 50_000_000_000; + + // Mapping to store configurable launch parameters for each token + mapping(address => BondingConfig.LaunchParams) public tokenLaunchParams; + + // Mapping to store graduation threshold for each token (calculated per-token based on airdropPercent and needAcf) + mapping(address => uint256) public tokenGradThreshold; + + event PreLaunched( + address indexed token, + address indexed pair, + uint256 virtualId, + uint256 initialPurchase, + BondingConfig.LaunchParams launchParams + ); + event Launched( + address indexed token, + address indexed pair, + uint256 virtualId, + uint256 initialPurchase, + uint256 initialPurchasedAmount, + BondingConfig.LaunchParams launchParams + ); + event CancelledLaunch( + address indexed token, + address indexed pair, + uint, + uint256 initialPurchase + ); + event Graduated(address indexed token, address agentToken); + + error InvalidTokenStatus(); + error InvalidInput(); + error SlippageTooHigh(); + error UnauthorizedLauncher(); + error LaunchModeNotEnabled(); + error InvalidAntiSniperType(); + error InvalidSpecialLaunchParams(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address factory_, + address router_, + address agentFactory_, + address bondingConfig_ + ) external initializer { + __Ownable_init(msg.sender); + __ReentrancyGuard_init(); + + factory = IFFactoryV2Minimal(factory_); + router = IFRouterV2Minimal(router_); + agentFactory = IAgentFactoryV6Minimal(agentFactory_); + bondingConfig = BondingConfig(bondingConfig_); + } + + function preLaunch( + string memory _name, + string memory _ticker, + uint8[] memory cores, + string memory desc, + string memory img, + string[4] memory urls, + uint256 purchaseAmount, + uint256 startTime, + uint8 launchMode_, + uint8 airdropPercent_, + bool needAcf_, + uint8 antiSniperTaxType_, + bool isProject60days_ + ) public nonReentrant returns (address, address, uint, uint256) { + // Fail-fast: validate reserve percentages and calculate bonding curve supply upfront + // This validates: airdropPercent <= maxAirdropPercent AND totalReserved < MAX_TOTAL_RESERVED_PERCENT + uint256 bondingCurveSupplyBase = bondingConfig.calculateBondingCurveSupply(airdropPercent_, needAcf_); + + // Validate anti-sniper tax type + if (!bondingConfig.isValidAntiSniperType(antiSniperTaxType_)) { + revert InvalidAntiSniperType(); + } + + if (cores.length <= 0) { + revert InvalidInput(); + } + + // Determine if this is an immediate launch or scheduled launch + // Immediate launch: startTime < now + scheduledLaunchStartTimeDelay + // Scheduled launch: startTime >= now + scheduledLaunchStartTimeDelay + BondingConfig.ScheduledLaunchParams memory scheduledParams = bondingConfig.scheduledLaunchParams(); + uint256 scheduledThreshold = block.timestamp + scheduledParams.startTimeDelay; + bool isScheduledLaunch = startTime >= scheduledThreshold; + + // Validate launch mode, authorization, and mode-specific requirements + _validateLaunchMode( + launchMode_, + antiSniperTaxType_, + airdropPercent_, + needAcf_, + isProject60days_, + isScheduledLaunch + ); + + uint256 actualStartTime; + uint256 startTimeDelay; + + if (isScheduledLaunch) { + // Scheduled launch: use provided startTime + actualStartTime = startTime; + startTimeDelay = scheduledParams.startTimeDelay; + } else { + // Immediate launch: start immediately + actualStartTime = block.timestamp; + startTimeDelay = 0; + } + + // Calculate launch fee based on launch type and ACF requirement + // Fee structure: + // - Immediate launch, no ACF: 0 + // - Immediate launch, with ACF: acfFee + // - Scheduled launch, no ACF: normalLaunchFee + // - Scheduled launch, with ACF: normalLaunchFee + acfFee + uint256 launchFee = bondingConfig.calculateLaunchFee(isScheduledLaunch, needAcf_); + + if (purchaseAmount < launchFee) { + revert InvalidInput(); + } + + address assetToken = router.assetToken(); + + uint256 initialPurchase = (purchaseAmount - launchFee); + if (launchFee > 0) { + IERC20(assetToken).safeTransferFrom(msg.sender, bondingConfig.feeTo(), launchFee); + } + IERC20(assetToken).safeTransferFrom( + msg.sender, + address(this), + initialPurchase + ); + + uint256 _initialSupply = bondingConfig.initialSupply(); + BondingConfig.DeployParams memory _deployParams = bondingConfig.deployParams(); + + (address token, uint256 applicationId) = agentFactory + .createNewAgentTokenAndApplication( + _name, // without "fun " prefix + _ticker, + abi.encode( + // tokenSupplyParams + _initialSupply, + 0, // lpSupply, will mint to agentTokenAddress + _initialSupply, // vaultSupply, will mint to vault + _initialSupply, + _initialSupply, + 0, + address(this) // vault, is the bonding contract itself + ), + cores, + _deployParams.tbaSalt, + _deployParams.tbaImplementation, + _deployParams.daoVotingPeriod, + _deployParams.daoThreshold, + 0, // applicationThreshold_ + msg.sender // token creator + ); + // this is to prevent transfer to blacklist address before graduation + agentFactory.addBlacklistAddress( + token, + IAgentTokenV2(token).liquidityPools()[0] + ); + + // Calculate bonding curve supply in wei (base supply was validated at the beginning) + uint256 bondingCurveSupply = bondingCurveSupplyBase * (10 ** IAgentTokenV2(token).decimals()); + // Calculate total reserved supply for transfer + uint256 totalReservedSupply = _initialSupply - bondingCurveSupplyBase; + + address _pair = factory.createPair( + token, + assetToken, + actualStartTime, + startTimeDelay + ); + + require(_approval(address(router), token, bondingCurveSupply)); + + // Get fixed fake initial virtual liquidity + uint256 liquidity = bondingConfig.getFakeInitialVirtualLiq(); + uint256 price = bondingCurveSupply / liquidity; + + router.addInitialLiquidity(token, bondingCurveSupply, liquidity); + + // Transfer reserved tokens (airdrop + ACF if needed) to teamTokenReservedWallet + if (totalReservedSupply > 0) { + IERC20(token).safeTransfer( + bondingConfig.teamTokenReservedWallet(), + totalReservedSupply * (10 ** IAgentTokenV2(token).decimals()) + ); + } + + // Calculate and store per-token graduation threshold + uint256 gradThreshold = bondingConfig.calculateGradThreshold(bondingCurveSupply); + tokenGradThreshold[token] = gradThreshold; + + tokenInfos.push(token); + + // Use storage reference to avoid stack overflow + BondingConfig.Token storage newToken = tokenInfo[token]; + newToken.creator = msg.sender; + newToken.token = token; + newToken.agentToken = address(0); + newToken.pair = _pair; + newToken.description = desc; + newToken.cores = cores; + newToken.image = img; + newToken.twitter = urls[0]; + newToken.telegram = urls[1]; + newToken.youtube = urls[2]; + newToken.website = urls[3]; + newToken.trading = true; + newToken.tradingOnUniswap = false; + newToken.applicationId = applicationId; + newToken.initialPurchase = initialPurchase; + newToken.virtualId = VirtualIdBase + tokenInfos.length; + newToken.launchExecuted = false; + + // Store V5 configurable launch parameters + tokenLaunchParams[token] = BondingConfig.LaunchParams({ + launchMode: launchMode_, + airdropPercent: airdropPercent_, + needAcf: needAcf_, + antiSniperTaxType: antiSniperTaxType_, + isProject60days: isProject60days_ + }); + + // Set Data struct fields + newToken.data.token = token; + newToken.data.name = _name; + newToken.data._name = _name; + newToken.data.ticker = _ticker; + newToken.data.supply = bondingCurveSupply; + newToken.data.price = price; + newToken.data.marketCap = liquidity; + newToken.data.liquidity = liquidity * 2; + newToken.data.volume = 0; + newToken.data.volume24H = 0; + newToken.data.prevPrice = price; + newToken.data.lastUpdated = block.timestamp; + + emit PreLaunched( + token, + _pair, + tokenInfo[token].virtualId, + initialPurchase, + tokenLaunchParams[token] + ); + + return (token, _pair, tokenInfo[token].virtualId, initialPurchase); + } + + function cancelLaunch(address _tokenAddress) public { + BondingConfig.Token storage _token = tokenInfo[_tokenAddress]; + + // Validate that the token exists and was properly prelaunched + if (_token.token == address(0) || _token.pair == address(0)) { + revert InvalidInput(); + } + + if (msg.sender != _token.creator) { + revert InvalidInput(); + } + + // Validate that the token has not been launched (or cancelled) + if (_token.launchExecuted) { + revert InvalidTokenStatus(); + } + + if (_token.initialPurchase > 0) { + IERC20(router.assetToken()).safeTransfer( + _token.creator, + _token.initialPurchase + ); + } + + _token.initialPurchase = 0; // prevent duplicate transfer initialPurchase back to the creator + _token.launchExecuted = true; // pretend it has been launched (cancelled) and prevent duplicate launch + + emit CancelledLaunch( + _tokenAddress, + _token.pair, + tokenInfo[_tokenAddress].virtualId, + _token.initialPurchase + ); + } + + function launch( + address _tokenAddress + ) public nonReentrant returns (address, address, uint, uint256) { + BondingConfig.Token storage _token = tokenInfo[_tokenAddress]; + + // Validate that the token exists and was properly prelaunched + if (_token.token == address(0) || _token.pair == address(0)) { + revert InvalidInput(); + } + + if (_token.launchExecuted) { + revert InvalidTokenStatus(); + } + + // If initialPurchase == 0, the function marks the token as launched + // while swaps remain blocked (since enabling depends solely on time), + // resulting in an inconsistent "launched but not tradable" state, also the 550M is go to teamTokenReservedWallet + // so we need to check the start time of the pair + IFPairV2 pair = IFPairV2(_token.pair); + if (block.timestamp < pair.startTime()) { + revert InvalidInput(); + } + + // Set tax start time to current block timestamp for proper anti-sniper tax calculation + router.setTaxStartTime(_token.pair, block.timestamp); + + // Make initial purchase for creator + // bondingContract will transfer initialPurchase $Virtual to pairAddress + // pairAddress will transfer amountsOut $agentToken to bondingContract + // bondingContract then will transfer all the amountsOut $agentToken to teamTokenReservedWallet + // in the BE, teamTokenReservedWallet will split it out for the initialBuy and 550M + uint256 amountOut = 0; + uint256 initialPurchase = _token.initialPurchase; + if (initialPurchase > 0) { + IERC20(router.assetToken()).forceApprove( + address(router), + initialPurchase + ); + amountOut = _buy( + address(this), + initialPurchase, // will raise error if initialPurchase <= 0 + _tokenAddress, + 0, + block.timestamp + 300, + true // isInitialPurchase = true for creator's purchase + ); + // creator's initialBoughtToken need to go to teamTokenReservedWallet for locking, not to creator + IERC20(_tokenAddress).safeTransfer( + bondingConfig.teamTokenReservedWallet(), + amountOut + ); + + // update initialPurchase and launchExecuted to prevent duplicate purchase + _token.initialPurchase = 0; + } + + emit Launched( + _tokenAddress, + _token.pair, + tokenInfo[_tokenAddress].virtualId, + initialPurchase, + amountOut, + tokenLaunchParams[_tokenAddress] + ); + _token.launchExecuted = true; + + return ( + _tokenAddress, + _token.pair, + tokenInfo[_tokenAddress].virtualId, + initialPurchase + ); + } + + function sell( + uint256 amountIn, + address tokenAddress, + uint256 amountOutMin, + uint256 deadline + ) public returns (bool) { + // this alrealy prevented it's a not-exists token + if (!tokenInfo[tokenAddress].trading) { + revert InvalidTokenStatus(); + } + + // this is to prevent sell before launch + if (!tokenInfo[tokenAddress].launchExecuted) { + revert InvalidTokenStatus(); + } + + if (block.timestamp > deadline) { + revert InvalidInput(); + } + + (uint256 amount0In, uint256 amount1Out) = router.sell( + amountIn, + tokenAddress, + msg.sender + ); + + if (amount1Out < amountOutMin) { + revert SlippageTooHigh(); + } + + uint256 duration = block.timestamp - + tokenInfo[tokenAddress].data.lastUpdated; + + if (duration > 86400) { + tokenInfo[tokenAddress].data.lastUpdated = block.timestamp; + } + + return true; + } + + function _buy( + address buyer, + uint256 amountIn, + address tokenAddress, + uint256 amountOutMin, + uint256 deadline, + bool isInitialPurchase + ) internal returns (uint256) { + if (block.timestamp > deadline) { + revert InvalidInput(); + } + address pairAddress = factory.getPair( + tokenAddress, + router.assetToken() + ); + + IFPairV2 pair = IFPairV2(pairAddress); + + (uint256 reserveA, uint256 reserveB) = pair.getReserves(); + + (uint256 amount1In, uint256 amount0Out) = router.buy( + amountIn, + tokenAddress, + buyer, + isInitialPurchase + ); + + if (amount0Out < amountOutMin) { + revert SlippageTooHigh(); + } + + uint256 newReserveA = reserveA - amount0Out; + uint256 duration = block.timestamp - + tokenInfo[tokenAddress].data.lastUpdated; + + if (duration > 86400) { + tokenInfo[tokenAddress].data.lastUpdated = block.timestamp; + } + + // Get per-token gradThreshold (calculated during preLaunch based on airdropPercent and needAcf) + uint256 gradThreshold = tokenGradThreshold[tokenAddress]; + + if ( + newReserveA <= gradThreshold && + !router.hasAntiSniperTax(pairAddress) && + tokenInfo[tokenAddress].trading + ) { + _openTradingOnUniswap(tokenAddress); + } + + return amount0Out; + } + + function buy( + uint256 amountIn, + address tokenAddress, + uint256 amountOutMin, + uint256 deadline + ) public payable returns (bool) { + // this alrealy prevented it's a not-exists token + if (!tokenInfo[tokenAddress].trading) { + revert InvalidTokenStatus(); + } + + // this is to prevent sell before launch + if (!tokenInfo[tokenAddress].launchExecuted) { + revert InvalidTokenStatus(); + } + + _buy(msg.sender, amountIn, tokenAddress, amountOutMin, deadline, false); + + return true; + } + + function _openTradingOnUniswap(address tokenAddress) private { + BondingConfig.Token storage _token = tokenInfo[tokenAddress]; + + if (_token.tradingOnUniswap || !_token.trading) { + revert InvalidTokenStatus(); + } + + // Transfer asset tokens to bonding contract + address pairAddress = factory.getPair( + tokenAddress, + router.assetToken() + ); + + IFPairV2 pair = IFPairV2(pairAddress); + + uint256 assetBalance = pair.assetBalance(); + uint256 tokenBalance = pair.balance(); + + router.graduate(tokenAddress); + + // previously initFromBondingCurve has two parts: + // 1. transfer applicationThreshold_ assetToken from bondingContract to agentFactoryV3Contract + // 2. create Application + // now only need to do 1st part and update application.withdrawableAmount to assetBalance + IERC20(router.assetToken()).safeTransfer( + address(agentFactory), + assetBalance + ); + agentFactory + .updateApplicationThresholdWithApplicationId( + _token.applicationId, + assetBalance + ); + + // remove blacklist address after graduation, cuz executeBondingCurveApplicationSalt we will transfer all left agentTokens to the uniswapV2Pair + agentFactory.removeBlacklistAddress( + tokenAddress, + IAgentTokenV2(tokenAddress).liquidityPools()[0] + ); + + // previously executeBondingCurveApplicationSalt will create agentToken and do two parts: + // 1. (lpSupply = all left $preToken in prePairAddress) $agentToken mint to agentTokenAddress + // 2. (vaultSupply = 1B - lpSupply) $agentToken mint to prePairAddress + // now only need to transfer (all left agentTokens) $agentTokens from agentFactoryV6Address to agentTokenAddress + IERC20(tokenAddress).safeTransfer(tokenAddress, tokenBalance); + require(_token.applicationId != 0, "ApplicationId not found"); + address agentToken = agentFactory + .executeBondingCurveApplicationSalt( + _token.applicationId, + _token.data.supply / 1 ether, // totalSupply + tokenBalance / 1 ether, // lpSupply + pairAddress, // vault + keccak256( + abi.encodePacked(msg.sender, block.timestamp, tokenAddress) + ) + ); + _token.agentToken = agentToken; + + // this is not needed, previously need to do this because of + // 1. (vaultSupply = 1B - lpSupply) $agentToken will mint to prePairAddress + // 2. then in unwrapToken, we will transfer burn preToken of each account and transfer same amount of agentToken to them from prePairAddress + // router.approval( + // pairAddress, + // agentToken, + // address(this), + // IERC20(agentToken).balanceOf(pairAddress) + // ); + + emit Graduated(tokenAddress, agentToken); + _token.trading = false; + _token.tradingOnUniswap = true; + } + + // View functions to check token launch type (affects tax recipient updates and liquidity drain permissions) + function isProject60days(address token) external view returns (bool) { + return tokenLaunchParams[token].isProject60days; + } + + function isProjectXLaunch(address token) external view returns (bool) { + return tokenLaunchParams[token].launchMode == bondingConfig.LAUNCH_MODE_X_LAUNCH(); + } + + function isAcpSkillLaunch(address token) external view returns (bool) { + return tokenLaunchParams[token].launchMode == bondingConfig.LAUNCH_MODE_ACP_SKILL(); + } + + // View function for FRouterV2 to get anti-sniper tax type + // Reverts if token was not created by BondingV5, allowing FRouterV2 to fallback to legacy logic + function tokenAntiSniperType(address token) external view returns (uint8) { + if (tokenInfo[token].creator == address(0)) { + revert InvalidTokenStatus(); + } + return tokenLaunchParams[token].antiSniperTaxType; + } + + function _approval( + address _spender, + address _token, + uint256 amount + ) internal returns (bool) { + IERC20(_token).forceApprove(_spender, amount); + + return true; + } + + /** + * @notice Validate launch mode is enabled and caller is authorized + * @param launchMode_ The launch mode identifier + */ + function _validateLaunchMode( + uint8 launchMode_, + uint8 antiSniperTaxType_, + uint8 airdropPercent_, + bool needAcf_, + bool isProject60days_, + bool isScheduledLaunch_ + ) internal view { + // Validate launch mode is one of the supported types + if (launchMode_ != bondingConfig.LAUNCH_MODE_NORMAL() && + launchMode_ != bondingConfig.LAUNCH_MODE_X_LAUNCH() && + launchMode_ != bondingConfig.LAUNCH_MODE_ACP_SKILL()) { + revert LaunchModeNotEnabled(); + } + + // Check authorization for special modes + if (launchMode_ == bondingConfig.LAUNCH_MODE_X_LAUNCH()) { + if (!bondingConfig.isXLauncher(msg.sender)) { + revert UnauthorizedLauncher(); + } + } else if (launchMode_ == bondingConfig.LAUNCH_MODE_ACP_SKILL()) { + if (!bondingConfig.isAcpSkillLauncher(msg.sender)) { + revert UnauthorizedLauncher(); + } + } + + // Special launch modes have strict requirements + // Only LAUNCH_MODE_NORMAL can freely configure parameters + if (bondingConfig.isSpecialMode(launchMode_)) { + // Special modes require: + // 1. ANTI_SNIPER_NONE + // 2. Immediate launch (startTime within 24h) + // 3. airdropPercent = 0 + // 4. needAcf = false + // 5. isProject60days_ = false + if (antiSniperTaxType_ != bondingConfig.ANTI_SNIPER_NONE() || + isScheduledLaunch_ || + airdropPercent_ != 0 || + needAcf_ || + isProject60days_) { + revert InvalidSpecialLaunchParams(); + } + } + } + + function setBondingConfig(address bondingConfig_) public onlyOwner { + bondingConfig = BondingConfig(bondingConfig_); + } +} + diff --git a/contracts/launchpadv2/FRouterV2.sol b/contracts/launchpadv2/FRouterV2.sol index 0c85ef0..e74a71e 100644 --- a/contracts/launchpadv2/FRouterV2.sol +++ b/contracts/launchpadv2/FRouterV2.sol @@ -16,6 +16,17 @@ interface IBondingV4ForRouter { function isProjectXLaunch(address token) external view returns (bool); } +// Minimal interface for BondingV5 to get anti-sniper tax type +interface IBondingV5ForRouter { + function tokenAntiSniperType(address token) external view returns (uint8); +} + +// Minimal interface for BondingConfig to get anti-sniper duration +interface IBondingConfigForRouter { + function getAntiSniperDuration(uint8 antiSniperType_) external pure returns (uint256); + function ANTI_SNIPER_NONE() external pure returns (uint8); +} + contract FRouterV2 is Initializable, AccessControlUpgradeable, @@ -34,6 +45,10 @@ contract FRouterV2 is // BondingV4 reference for checking X_LAUNCH tokens IBondingV4ForRouter public bondingV4; + // BondingV5 reference for checking anti-sniper tax type + IBondingV5ForRouter public bondingV5; + IBondingConfigForRouter public bondingConfig; + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -244,6 +259,18 @@ contract FRouterV2 is bondingV4 = IBondingV4ForRouter(bondingV4_); } + /** + * @dev Set the BondingV5 and BondingConfig contract addresses for anti-sniper tax type checks + * @param bondingV5_ The address of the BondingV5 contract + * @param bondingConfig_ The address of the BondingConfig contract + */ + function setBondingV5(address bondingV5_, address bondingConfig_) public onlyRole(ADMIN_ROLE) { + require(bondingV5_ != address(0), "Zero address not allowed"); + require(bondingConfig_ != address(0), "Zero address not allowed"); + bondingV5 = IBondingV5ForRouter(bondingV5_); + bondingConfig = IBondingConfigForRouter(bondingConfig_); + } + function resetTime( address tokenAddress, uint256 newStartTime @@ -257,8 +284,9 @@ contract FRouterV2 is /** * @dev Calculate anti-sniper tax based on time elapsed since pair start - * For regular tokens: Tax starts at 99% and decreases by 1% per minute to 0% over 99 minutes - * For X_LAUNCH tokens: Tax starts at 99% and decreases by 1% per second to 0% over 99 seconds + * BondingV5 tokens: Use configurable anti-sniper types (NONE=0s, 60S=60s, 98M=98min) + * BondingV4 X_LAUNCH tokens: Tax decreases from 99% to 0% over 99 seconds + * Legacy tokens: Tax decreases from 99% to 0% over 99 minutes * @param pairAddress The address of the pair * @return taxPercentage Tax in percentage (1 = 1%) */ @@ -270,28 +298,78 @@ contract FRouterV2 is // Get token address directly from pair (tokenA is the agent token) address tokenAddress = pair.tokenA(); - uint256 finalTaxStartTime = pair.startTime(); - // Try to get taxStartTime safely for backward compatibility - uint256 taxStartTime = 0; - try pair.taxStartTime() returns (uint256 _taxStartTime) { - taxStartTime = _taxStartTime; - } catch { - // Old pair contract doesn't have taxStartTime function - // Use startTime for backward compatibility - taxStartTime = 0; + uint256 startTax = factory.antiSniperBuyTaxStartValue(); // 99% + + // Check if this is a BondingV5 token with configurable anti-sniper type + if (address(bondingV5) != address(0) && address(bondingConfig) != address(0)) { + (bool found, uint256 tax) = _calculateBondingV5AntiSniperTax(tokenAddress, pair, startTax); + if (found) { + return tax; + } } - if (taxStartTime > 0) { - finalTaxStartTime = taxStartTime; // use taxStartTime if it's set (for new pairs) + + // Legacy logic for BondingV4 and older tokens + return _calculateLegacyAntiSniperTax(tokenAddress, pair, startTax); + } + + /** + * @dev Calculate anti-sniper tax for BondingV5 tokens + * @return found Whether the token was found in BondingV5 + * @return tax The calculated tax (0-99) + */ + function _calculateBondingV5AntiSniperTax( + address tokenAddress, + IFPairV2 pair, + uint256 startTax + ) private view returns (bool found, uint256 tax) { + try bondingV5.tokenAntiSniperType(tokenAddress) returns (uint8 antiSniperType) { + // Get the duration for this anti-sniper type + uint256 duration = bondingConfig.getAntiSniperDuration(antiSniperType); + + // ANTI_SNIPER_NONE: no tax at all + if (duration == 0) { + return (true, 0); + } + + // Get tax start time + uint256 taxStartTime = _getTaxStartTime(pair); + + // If trading hasn't started yet, use maximum tax + if (block.timestamp < taxStartTime) { + return (true, startTax); + } + + uint256 timeElapsed = block.timestamp - taxStartTime; + + // If time elapsed exceeds duration, no tax + if (timeElapsed >= duration) { + return (true, 0); + } + + // Linear decrease: tax = startTax * (duration - timeElapsed) / duration + return (true, (startTax * (duration - timeElapsed)) / duration); + } catch { + // Token not in BondingV5 + return (false, 0); } + } - uint256 startTax = factory.antiSniperBuyTaxStartValue(); // 99% + /** + * @dev Calculate anti-sniper tax for legacy tokens (BondingV4 and older) + */ + function _calculateLegacyAntiSniperTax( + address tokenAddress, + IFPairV2 pair, + uint256 startTax + ) private view returns (uint256) { + uint256 taxStartTime = _getTaxStartTime(pair); // If trading hasn't started yet, use maximum tax - if (block.timestamp < finalTaxStartTime) { + if (block.timestamp < taxStartTime) { return startTax; } - uint256 timeElapsed = block.timestamp - finalTaxStartTime; + uint256 timeElapsed = block.timestamp - taxStartTime; // Check if this is an X_LAUNCH token (shorter anti-sniper period) bool isXLaunch = false; @@ -318,6 +396,25 @@ contract FRouterV2 is return startTax - taxReduction; } + /** + * @dev Get the effective tax start time for a pair + * @param pair The pair contract + * @return The tax start time (taxStartTime if set, otherwise startTime) + */ + function _getTaxStartTime(IFPairV2 pair) private view returns (uint256) { + uint256 finalTaxStartTime = pair.startTime(); + // Try to get taxStartTime safely for backward compatibility + try pair.taxStartTime() returns (uint256 _taxStartTime) { + if (_taxStartTime > 0) { + finalTaxStartTime = _taxStartTime; // use taxStartTime if it's set (for new pairs) + } + } catch { + // Old pair contract doesn't have taxStartTime function + // Use startTime for backward compatibility + } + return finalTaxStartTime; + } + function hasAntiSniperTax(address pairAddress) public view returns (bool) { return _calculateAntiSniperTax(pairAddress) > 0; } diff --git a/hardhat.config.js b/hardhat.config.js index b7f2498..d055972 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -82,6 +82,10 @@ module.exports = { url: "https://sepolia.drpc.org", accounts: [process.env.PRIVATE_KEY], }, + eth_sepolia: { + url: "https://sepolia.drpc.org", + accounts: [process.env.PRIVATE_KEY], + }, base: { url: process.env.BASE_RPC_URL || diff --git a/scripts/launchpadv2/deployLaunchpadv2.ts b/scripts/launchpadv2/deployLaunchpadv2.ts index f55bf7a..0a1f2fe 100644 --- a/scripts/launchpadv2/deployLaunchpadv2.ts +++ b/scripts/launchpadv2/deployLaunchpadv2.ts @@ -111,10 +111,6 @@ const { ethers, upgrades } = require("hardhat"); if (!uniswapV2Factory) { throw new Error("UNISWAP_V2_FACTORY not set in environment"); } - const uniswapV2Router = process.env.UNISWAP_V2_ROUTER; - if (!uniswapV2Router) { - throw new Error("UNISWAP_V2_ROUTER not set in environment"); - } const agentNftV2 = process.env.AGENT_NFT_V2; if (!agentNftV2) { throw new Error("AGENT_NFT_V2 not set in environment"); diff --git a/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts b/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts new file mode 100644 index 0000000..b4b7fd4 --- /dev/null +++ b/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts @@ -0,0 +1,632 @@ +import { parseEther } from "ethers"; +const { ethers, upgrades } = require("hardhat"); + +(async () => { + try { + console.log("\n=== NewLaunchpad Deployment Starting ==="); + + // Basic check for .env variables + const deployerAddress = process.env.DEPLOYER; + if (!deployerAddress) { + throw new Error("DEPLOYER not set in environment"); + } + const beOpsWallet = process.env.BE_OPS_WALLET; + if (!beOpsWallet) { + throw new Error("BE_OPS_WALLET not set in environment"); + } + const contractController = process.env.CONTRACT_CONTROLLER; + if (!contractController) { + throw new Error("CONTRACT_CONTROLLER not set in environment"); + } + const admin = process.env.ADMIN; + if (!admin) { + throw new Error("ADMIN not set in environment"); + } + + // Load arguments directly from environment variables + const virtualToken = process.env.BRIDGED_TOKEN; + if (!virtualToken) { + throw new Error("BRIDGED_TOKEN not set in environment"); + } + const prototypeBuyTax = process.env.PROTOTYPE_BUY_TAX; + if (!prototypeBuyTax) { + throw new Error("PROTOTYPE_BUY_TAX not set in environment"); + } + const prototypeSellTax = process.env.PROTOTYPE_SELL_TAX; + if (!prototypeSellTax) { + throw new Error("PROTOTYPE_SELL_TAX not set in environment"); + } + + const sentientBuyTax = process.env.SENTIENT_BUY_TAX; + if (!sentientBuyTax) { + throw new Error("SENTIENT_BUY_TAX not set in environment"); + } + const sentientSellTax = process.env.SENTIENT_SELL_TAX; + if (!sentientSellTax) { + throw new Error("SENTIENT_SELL_TAX not set in environment"); + } + const antiSniperBuyTaxStartValue = + process.env.ANTI_SNIPER_BUY_TAX_START_VALUE; + if (!antiSniperBuyTaxStartValue) { + throw new Error("ANTI_SNIPER_BUY_TAX_START_VALUE not set in environment"); + } + const creationFeeToAddress = + process.env.LAUNCHPAD_V2_CREATION_FEE_TO_ADDRESS; + if (!creationFeeToAddress) { + throw new Error("LAUNCHPAD_V2_FEE_ADDRESS not set in environment"); + } + const feeAmount = process.env.LAUNCHPAD_V2_FEE_AMOUNT; + if (!feeAmount) { + throw new Error("LAUNCHPAD_V2_FEE_AMOUNT not set in environment"); + } + const initialSupply = process.env.INITIAL_SUPPLY; + if (!initialSupply) { + throw new Error("INITIAL_SUPPLY not set in environment"); + } + const assetRate = process.env.ASSET_RATE; + if (!assetRate) { + throw new Error("ASSET_RATE not set in environment"); + } + const maxTx = process.env.MAX_TX; + if (!maxTx) { + throw new Error("MAX_TX not set in environment"); + } + const gradThreshold = process.env.GRAD_THRESHOLD; + if (!gradThreshold) { + throw new Error("GRAD_THRESHOLD not set in environment"); + } + const startTimeDelay = process.env.LAUNCHPAD_V2_START_TIME_DELAY; + if (!startTimeDelay) { + throw new Error("LAUNCHPAD_V2_START_TIME_DELAY not set in environment"); + } + const tbaSalt = process.env.TBA_SALT; + if (!tbaSalt) { + throw new Error("TBA_SALT not set in environment"); + } + const tbaRegistry = process.env.TBA_REGISTRY; + if (!tbaRegistry) { + throw new Error("TBA_REGISTRY not set in environment"); + } + const tbaImplementation = process.env.TBA_IMPLEMENTATION; + if (!tbaImplementation) { + throw new Error("TBA_IMPLEMENTATION not set in environment"); + } + const daoVotingPeriod = process.env.DAO_VOTING_PERIOD; + if (!daoVotingPeriod) { + throw new Error("DAO_VOTING_PERIOD not set in environment"); + } + const daoThreshold = process.env.DAO_THRESHOLD; + if (!daoThreshold) { + throw new Error("DAO_THRESHOLD not set in environment"); + } + const teamTokenReservedSupply = process.env.TEAM_TOKEN_RESERVED_SUPPLY; + if (!teamTokenReservedSupply) { + throw new Error("TEAM_TOKEN_RESERVED_SUPPLY not set in environment"); + } + const teamTokenReservedWallet = process.env.TEAM_TOKEN_RESERVED_WALLET; + if (!teamTokenReservedWallet) { + throw new Error("TEAM_TOKEN_RESERVED_WALLET not set in environment"); + } + const uniswapV2Factory = process.env.UNISWAP_V2_FACTORY; + if (!uniswapV2Factory) { + throw new Error("UNISWAP_V2_FACTORY not set in environment"); + } + const uniswapV2Router = process.env.UNISWAP_V2_ROUTER; + if (!uniswapV2Router) { + throw new Error("UNISWAP_V2_ROUTER not set in environment"); + } + const agentNftV2 = process.env.AGENT_NFT_V2; + if (!agentNftV2) { + throw new Error("AGENT_NFT_V2 not set in environment"); + } + // FFactoryV2_TAX_VAULT must be a taxManager contract, but if we can + // make sure FFactoryV2_TAX_VAULT != FRouter.taxManager, then FFactoryV2_TAX_VAULT can be an EOA wallet + const fFactoryV2TaxVault = process.env.FFactoryV2_TAX_VAULT; + if (!fFactoryV2TaxVault) { + throw new Error("FFactoryV2_TAX_VAULT not set in environment"); + } + const fRouterV2TaxManager = process.env.FRouterV2_TAX_MANAGER; + if (!fRouterV2TaxManager) { + throw new Error("FRouterV2_TAX_MANAGER not set in environment"); + } + const agentDAO = process.env.AGENT_DAO; + if (!agentDAO) { + throw new Error("AGENT_DAO not set in environment"); + } + const agentFactoryV6Vault = process.env.AGENT_FACTORY_V6_VAULT; + if (!agentFactoryV6Vault) { + throw new Error("AGENT_FACTORY_V6_VAULT not set in environment"); + } + const agentFactoryV6MaturityDuration = + process.env.AGENT_FACTORY_V6_Maturity_Duration; + if (!agentFactoryV6MaturityDuration) { + throw new Error( + "AGENT_FACTORY_V6_Maturity_Duration not set in environment" + ); + } + const agentFactoryV6NextId = process.env.AGENT_FACTORY_V6_NEXT_ID; + if (!agentFactoryV6NextId) { + throw new Error("AGENT_FACTORY_V6_NEXT_ID not set in environment"); + } + const antiSniperTaxVaultAddress = process.env.ANTI_SNIPER_TAX_VAULT; + if (!antiSniperTaxVaultAddress) { + throw new Error("ANTI_SNIPER_TAX_VAULT not set in environment"); + } + const taxSwapThresholdBasisPoints = + process.env.TAX_SWAP_THRESHOLD_BASIS_POINTS; + if (!taxSwapThresholdBasisPoints) { + throw new Error("TAX_SWAP_THRESHOLD_BASIS_POINTS not set in environment"); + } + + + const agentTokenTaxManager = + process.env.AGENT_TOKEN_TAX_MANAGER; + if (!agentTokenTaxManager) { + throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + } + + console.log("Deployment arguments loaded:", { + virtualToken, + buyTax: prototypeBuyTax, + sellTax: prototypeSellTax, + factoryBuyTax: sentientBuyTax, + factorySellTax: sentientSellTax, + antiSniperBuyTaxStartValue, + feeAddress: creationFeeToAddress, + feeAmount, + initialSupply, + assetRate, + maxTx, + gradThreshold, + startTimeDelay, + tbaSalt, + tbaRegistry, + tbaImplementation, + daoVotingPeriod, + daoThreshold, + teamTokenReservedSupply, + uniswapV2Factory, + uniswapV2Router, + agentNftV2, + fFactoryV2TaxVault: fFactoryV2TaxVault, + fRouterV2TaxManager: fRouterV2TaxManager, + agentDAO, + agentFactoryV6Vault, + agentFactoryV6MaturityDuration, + agentFactoryV6NextId, + antiSniperTaxVaultAddress, + taxSwapThresholdBasisPoints, + agentTokenTaxManager + }); + + // 1. Deploy FFactoryV2, must happen before FRouterV2, + // because FRouterV2 cannot setFactory later + console.log("\n--- Deploying FFactoryV2 ---"); + const FFactoryV2 = await ethers.getContractFactory("FFactoryV2"); + const fFactoryV2 = await upgrades.deployProxy( + FFactoryV2, + [ + fFactoryV2TaxVault, // taxVault + prototypeBuyTax, // buyTax + prototypeSellTax, // sellTax + antiSniperBuyTaxStartValue, // antiSniperBuyTaxStartValue + antiSniperTaxVaultAddress, // antiSniperTaxVault + ], + { + initializer: "initialize", + initialOwner: process.env.CONTRACT_CONTROLLER, + } + ); + await fFactoryV2.waitForDeployment(); + const fFactoryV2Address = await fFactoryV2.getAddress(); + console.log("FFactoryV2 deployed at:", fFactoryV2Address); + + // 2. Deploy FRouterV2 + console.log("\n--- Deploying FRouterV2 ---"); + const FRouterV2 = await ethers.getContractFactory("FRouterV2"); + const fRouterV2 = await upgrades.deployProxy( + FRouterV2, + [ + fFactoryV2Address, // factory + virtualToken, // virtualToken (assetToken) + ], + { + initializer: "initialize", + initialOwner: process.env.CONTRACT_CONTROLLER, + } + ); + await fRouterV2.waitForDeployment(); + const fRouterV2Address = await fRouterV2.getAddress(); + console.log("FRouterV2 deployed at:", fRouterV2Address); + + // 3. Grant ADMIN_ROLE of FFactoryV2 to deployer temporarily, for setRouter and setTaxParams + console.log( + "\n--- Granting ADMIN_ROLE of FFactoryV2 to deployer temporarily ---" + ); + const signers = await ethers.getSigners(); + const deployer = signers[0]; + const tx0 = await fFactoryV2.grantRole( + await fFactoryV2.ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx0.wait(); + console.log("ADMIN_ROLE of FFactoryV2 granted to deployer temporarily"); + + // 4.0 Grant ADMIN_ROLE of FFactoryV2 to ADMIN, for setRouter and setTaxParams + console.log("\n--- Granting ADMIN_ROLE to ADMIN ---"); + const tx0_5 = await fFactoryV2.grantRole( + await fFactoryV2.ADMIN_ROLE(), + process.env.ADMIN + ); + await tx0_5.wait(); + console.log("ADMIN_ROLE granted to ADMIN"); + + // 4.1 set fRouterV2Address in FFactoryV2 + console.log("\n--- Setting fRouterV2Address in FFactoryV2 ---"); + const tx1 = await fFactoryV2.setRouter(fRouterV2Address); + await tx1.wait(); + console.log("fRouterV2Address set in FFactoryV2"); + + // 7. Grant ADMIN_ROLE of fRouterV2 to deployer temporarily, for setTaxManager and setAntiSniperTaxManager + console.log( + "\n--- Granting ADMIN_ROLE of fRouterV2 to deployer temporarily ---" + ); + const tx3_1 = await fRouterV2.grantRole( + await fRouterV2.ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx3_1.wait(); + console.log("ADMIN_ROLE of fRouterV2 granted to deployer temporarily"); + + // 8. Set taxManager for FRouterV2 + console.log("\n--- Setting TaxManager for FRouterV2 ---"); + const tx4_1 = await fRouterV2.setTaxManager(fRouterV2TaxManager); + await tx4_1.wait(); + console.log("TaxManager set for FRouterV2: ", fRouterV2TaxManager); + + // 9. Set antiSniperTaxManager for FRouterV2 + console.log("\n--- Setting AntiSniperTaxManager for FRouterV2 ---"); + const tx5_1 = await fRouterV2.setAntiSniperTaxManager( + antiSniperTaxVaultAddress + ); + await tx5_1.wait(); + console.log("AntiSniperTaxManager set for FRouterV2"); + + // 10. Deploy AgentTokenV2 implementation + console.log("\n--- Deploying AgentTokenV2 implementation ---"); + const AgentTokenV2 = await ethers.getContractFactory("AgentTokenV2"); + const agentTokenV2 = await AgentTokenV2.deploy(); + await agentTokenV2.waitForDeployment(); + const agentTokenV2Address = await agentTokenV2.getAddress(); + console.log( + "AgentTokenV2 implementation deployed at:", + agentTokenV2Address + ); + + // 11. Deploy AgentVeTokenV2 implementation + console.log("\n--- Deploying AgentVeTokenV2 implementation ---"); + const AgentVeTokenV2 = await ethers.getContractFactory("AgentVeTokenV2"); + const agentVeTokenV2 = await AgentVeTokenV2.deploy(); + await agentVeTokenV2.waitForDeployment(); + const agentVeTokenV2Address = await agentVeTokenV2.getAddress(); + console.log( + "AgentVeTokenV2 implementation deployed at:", + agentVeTokenV2Address + ); + + // 12. Deploy AgentFactoryV6 + console.log("\n--- Deploying AgentFactoryV6 ---"); + const AgentFactoryV6 = await ethers.getContractFactory("AgentFactoryV6"); + const agentFactoryV6 = await upgrades.deployProxy( + AgentFactoryV6, + [ + agentTokenV2Address, // tokenImplementation_ + agentVeTokenV2Address, // veTokenImplementation_ + agentDAO, // daoImplementation_ + tbaRegistry, // tbaRegistry_ + virtualToken, // assetToken_ (virtualToken) + agentNftV2, // nft_ (AgentNftV2) + agentFactoryV6Vault, // vault_, who will hold all the NFTs + agentFactoryV6NextId, // nextId_ + ], + { + initializer: "initialize", + initialOwner: process.env.CONTRACT_CONTROLLER, + } + ); + await agentFactoryV6.waitForDeployment(); + const agentFactoryV6Address = await agentFactoryV6.getAddress(); + console.log("AgentFactoryV6 deployed at:", agentFactoryV6Address); + + // 14. Grant DEFAULT_ADMIN_ROLE to deployer temporarily for AgentFactoryV6, for setParams() + console.log( + "\n--- Granting DEFAULT_ADMIN_ROLE to deployer temporarily for AgentFactoryV6 ---" + ); + const tx7_1 = await agentFactoryV6.grantRole( + await agentFactoryV6.DEFAULT_ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx7_1.wait(); + console.log("DEFAULT_ADMIN_ROLE granted to deployer"); + + // 15. setParams() for AgentFactoryV6 + console.log("\n--- Setting params for AgentFactoryV6 ---"); + const tx8_1 = await agentFactoryV6.setParams( + agentFactoryV6MaturityDuration, // maturityDuration + uniswapV2Router, // uniswapRouter + process.env.ADMIN, // defaultDelegatee, + process.env.ADMIN // tokenAdmin, + ); + await tx8_1.wait(); + console.log("setParams() called successfully for AgentFactoryV6"); + + // 18. Deploy BondingV2 + console.log("\n--- Deploying BondingV2 ---"); + const BondingV2 = await ethers.getContractFactory("BondingV2"); + const bondingV2 = await upgrades.deployProxy( + BondingV2, + [ + fFactoryV2Address, // factory_ + fRouterV2Address, // router_ + creationFeeToAddress, // feeTo_ (fee address) + feeAmount, // fee_ (fee amount) + initialSupply, // initialSupply_ + assetRate, // assetRate_ + maxTx, // maxTx_ + agentFactoryV6Address, // agentFactory_ + gradThreshold, // gradThreshold_ + startTimeDelay, // startTimeDelay_ + ], + { + initializer: "initialize", + initialOwner: process.env.CONTRACT_CONTROLLER, + } + ); + await bondingV2.waitForDeployment(); + const bondingV2Address = await bondingV2.getAddress(); + console.log("BondingV2 deployed at:", bondingV2Address); + + // 17. Set token params for AgentFactoryV6 + console.log("\n--- Setting token params for AgentFactoryV6 ---"); + const tx3 = await agentFactoryV6.setTokenParams( + sentientBuyTax, // projectBuyTaxBasisPoints (sentientBuyTax) + sentientSellTax, // projectSellTaxBasisPoints (sentientSellTax) + taxSwapThresholdBasisPoints, // taxSwapThresholdBasisPoints, todo: configurable VP demon + agentTokenTaxManager // projectTaxRecipient (fee address) + ); + await tx3.wait(); + console.log("Token params set for AgentFactoryV6"); + + // 19. Set DeployParams and LaunchParams for BondingV2 (deployer is owner initially) + console.log("\n--- Setting DeployParams for BondingV2 ---"); + const deployParams = { + tbaSalt: tbaSalt, // tbaSalt + tbaImplementation: tbaImplementation, // tbaImplementation + daoVotingPeriod: daoVotingPeriod, // daoVotingPeriod + daoThreshold: daoThreshold, // daoThreshold + }; + const tx4 = await bondingV2.setDeployParams(deployParams); + await tx4.wait(); + console.log("DeployParams set for BondingV2"); + + // 20. Set LaunchParams for BondingV2 + console.log("\n--- Setting LaunchParams for BondingV2 ---"); + const launchParams = { + startTimeDelay: startTimeDelay, // startTimeDelay + teamTokenReservedSupply: teamTokenReservedSupply, // teamTokenReservedSupply + teamTokenReservedWallet: teamTokenReservedWallet, // teamTokenReservedWallet + }; + const tx5 = await bondingV2.setLaunchParams(launchParams); + await tx5.wait(); + console.log("LaunchParams set for BondingV2"); + + // 22. Grant necessary roles and Transfer ownership + console.log("\n--- Granting necessary roles, Transfer ownership ---"); + + // 22.1 Grant DEFAULT_ADMIN_ROLE of FFactoryV2 to admin + console.log("\n--- Granting DEFAULT_ADMIN_ROLE of FFactoryV2 to ADMIN ---"); + const tx6 = await fFactoryV2.grantRole( + await fFactoryV2.DEFAULT_ADMIN_ROLE(), + process.env.ADMIN + ); + await tx6.wait(); + console.log( + "Granted DEFAULT_ADMIN_ROLE of FFactoryV2 to ADMIN:", + process.env.ADMIN + ); + + // 22.2 Grant CREATOR_ROLE of FFactoryV2 to BondingV2, for createPair() + const tx10 = await fFactoryV2.grantRole( + await fFactoryV2.CREATOR_ROLE(), + bondingV2Address + ); + await tx10.wait(); + console.log( + "Granted CREATOR_ROLE of FFactoryV2 to BondingV2:", + bondingV2Address + ); + + // 22.3 Grant DEFAULT_ADMIN_ROLE of FRouterV2 to ADMIN + const tx6_5 = await fRouterV2.grantRole( + await fRouterV2.DEFAULT_ADMIN_ROLE(), + process.env.ADMIN + ); + await tx6_5.wait(); + console.log( + "Granted DEFAULT_ADMIN_ROLE of FRouterV2 to ADMIN:", + process.env.ADMIN + ); + + // 22.4 Grant EXECUTOR_ROLE of FRouterV2 to BondingV2, for buy(), sell(), addInitialLiquidity() + console.log("\n--- Granting EXECUTOR_ROLE of FRouterV2 to BondingV2 ---"); + const tx7 = await fRouterV2.grantRole( + await fRouterV2.EXECUTOR_ROLE(), + bondingV2Address + ); + await tx7.wait(); + console.log( + "Granted EXECUTOR_ROLE of FRouterV2 to BondingV2:", + bondingV2Address + ); + + // 22.5 Grant EXECUTOR_ROLE of FRouterV2 to BE_OPS_WALLET, for resetTime() + const tx8 = await fRouterV2.grantRole( + await fRouterV2.EXECUTOR_ROLE(), + process.env.BE_OPS_WALLET + ); + await tx8.wait(); + console.log( + "Granted EXECUTOR_ROLE of FRouterV2 to BE_OPS_WALLET:", + process.env.BE_OPS_WALLET + ); + + // 22.6 Grant DEFAULT_ADMIN_ROLE of AgentFactoryV6 to ADMIN + console.log("\n--- Granting DEFAULT_ADMIN_ROLE of AgentFactoryV6 to ADMIN ---"); + const tx8_2 = await agentFactoryV6.grantRole( + await agentFactoryV6.DEFAULT_ADMIN_ROLE(), + process.env.ADMIN + ); + await tx8_2.wait(); + console.log( + "Granted DEFAULT_ADMIN_ROLE of AgentFactoryV6 to ADMIN:", + process.env.ADMIN + ); + + // 22.7 Grant BONDING_ROLE of AgentFactoryV6 to BondingV2, + // for createNewAgentTokenAndApplication(), updateApplicationThresholdWithApplicationId() + // for executeBondingCurveApplicationSalt(), + // for addBlacklistAddress(), removeBlacklistAddress() + const tx9 = await agentFactoryV6.grantRole( + await agentFactoryV6.BONDING_ROLE(), + bondingV2Address + ); + await tx9.wait(); + console.log( + "Granted BONDING_ROLE of AgentFactoryV6 to BondingV2:", + bondingV2Address + ); + + // 22.8 Grant REMOVE_LIQUIDITY_ROLE of AgentFactoryV6 to BondingV2, for removeLiquidity() + const tx9_1 = await agentFactoryV6.grantRole( + await agentFactoryV6.REMOVE_LIQUIDITY_ROLE(), + admin + ); + await tx9_1.wait(); + console.log( + "Granted REMOVE_LIQUIDITY_ROLE of AgentFactoryV6 to ADMIN:", + admin + ); + + // 22.9 Transfer ownership of BondingV2 to CONTRACT_CONTROLLER + console.log( + "\n--- Transferring BondingV2 ownership to CONTRACT_CONTROLLER ---" + ); + const tx3_5 = await bondingV2.transferOwnership( + process.env.CONTRACT_CONTROLLER + ); + await tx3_5.wait(); + console.log("BondingV2 ownership transferred to CONTRACT_CONTROLLER"); + + // 23. Revoke deployer roles (security best practice) + console.log("\n--- Revoking deployer roles ---"); + + // 23.1 Revoke deployer roles from FFactoryV2 + const tx13 = await fFactoryV2.revokeRole( + await fFactoryV2.ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx13.wait(); + console.log( + "Revoked ADMIN_ROLE of FFactoryV2 from Deployer:", + await deployer.getAddress() + ); + + // 23.2 Revoke deployer roles from FRouterV2 + const tx14 = await fRouterV2.revokeRole( + await fRouterV2.ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx14.wait(); + console.log( + "Revoked ADMIN_ROLE of FRouterV2 from Deployer:", + await deployer.getAddress() + ); + + // 23.3 Revoke deployer roles from AgentFactoryV6 + const tx15 = await agentFactoryV6.revokeRole( + await agentFactoryV6.DEFAULT_ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx15.wait(); + console.log( + "Revoked DEFAULT_ADMIN_ROLE of AgentFactoryV6 from Deployer:", + await deployer.getAddress() + ); + + // AccessControlUpgradeable (FFactoryV2, FRouterV2): + // Automatically grants DEFAULT_ADMIN_ROLE to the deployer during deployment + + // AccessControl (AgentFactoryV6): + // Does NOT automatically grant DEFAULT_ADMIN_ROLE to anyone + + // 23.4 Revoke default admin role of ffactoryv2 from deployer + console.log("\n--- Revoking DEFAULT_ADMIN_ROLE of FFactoryV2 from Deployer ---"); + const tx16 = await fFactoryV2.revokeRole( + await fFactoryV2.DEFAULT_ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx16.wait(); + console.log("Revoked DEFAULT_ADMIN_ROLE of FFactoryV2 from Deployer:", await deployer.getAddress()); + + // 23.5 Revoke default admin role of frouterv2 from deployer + console.log("\n--- Revoking DEFAULT_ADMIN_ROLE of FRouterV2 from Deployer ---"); + const tx17 = await fRouterV2.revokeRole( + await fRouterV2.DEFAULT_ADMIN_ROLE(), + await deployer.getAddress() + ); + await tx17.wait(); + console.log("Revoked DEFAULT_ADMIN_ROLE of FRouterV2 from Deployer:", await deployer.getAddress()); + + // 24. Grant MINTER_ROLE of agentNftV2 to agentFactoryV6 + // Need admin signer because only admin has DEFAULT_ADMIN_ROLE on AgentNftV2 + console.log( + "\n--- Granting MINTER_ROLE of agentNftV2 to agentFactoryV6 ---" + ); + if (!process.env.ADMIN_PRIVATE_KEY) { + console.log("⚠️ ADMIN_PRIVATE_KEY not set - skipping MINTER_ROLE grant"); + console.log("⚠️ Admin needs to manually grant MINTER_ROLE of AgentNftV2 to AgentFactoryV6:", agentFactoryV6Address); + } else { + const adminSigner = new ethers.Wallet( + process.env.ADMIN_PRIVATE_KEY, + ethers.provider + ); + const agentNftV2Contract = await ethers.getContractAt( + "AgentNftV2", + agentNftV2, + adminSigner + ); + const tx6_1 = await agentNftV2Contract.grantRole( + await agentNftV2Contract.MINTER_ROLE(), + agentFactoryV6Address + ); + await tx6_1.wait(); + console.log("✅ MINTER_ROLE of agentNftV2 granted to agentFactoryV6"); + } + + // 24. Print deployment summary + console.log("\n=== NewLaunchpad Deployment Summary ==="); + console.log("All contracts deployed and configured:"); + console.log("- AgentTokenV2 (implementation):", agentTokenV2Address); + console.log("- AgentVeTokenV2 (implementation):", agentVeTokenV2Address); + console.log("- AgentDAO (implementation):", agentDAO); + console.log("- FFactoryV2:", fFactoryV2Address); + console.log("- FRouterV2:", fRouterV2Address); + console.log("- AgentFactoryV6:", agentFactoryV6Address); + console.log("- BondingV2:", bondingV2Address); + + console.log("\nDeployment and role setup completed successfully!"); + } catch (e) { + console.error("Deployment failed:", e); + throw e; + } +})(); diff --git a/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts b/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts new file mode 100644 index 0000000..0f93fa0 --- /dev/null +++ b/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts @@ -0,0 +1,148 @@ +import { parseEther } from "ethers"; +const { ethers, upgrades } = require("hardhat"); + +(async () => { + try { + console.log("\n=== Prerequisites Deployment Starting ==="); + console.log("This script deploys: AgentDAO, AgentNftV2, AgentTax (AGENT_TOKEN_TAX_MANAGER)"); + + // Basic check for .env variables + const deployerAddress = process.env.DEPLOYER; + if (!deployerAddress) { + throw new Error("DEPLOYER not set in environment"); + } + const contractController = process.env.CONTRACT_CONTROLLER; + if (!contractController) { + throw new Error("CONTRACT_CONTROLLER not set in environment"); + } + const admin = process.env.ADMIN; + if (!admin) { + throw new Error("ADMIN not set in environment"); + } + + // Required for AgentTax + const assetToken = process.env.BRIDGED_TOKEN; + if (!assetToken) { + throw new Error("BRIDGED_TOKEN not set in environment"); + } + const taxToken = process.env.AGENT_TAX_TOKEN; + if (!taxToken) { + throw new Error("AGENT_TAX_TOKEN not set in environment"); + } + const uniswapV2Router = process.env.UNISWAP_V2_ROUTER; + if (!uniswapV2Router) { + throw new Error("UNISWAP_V2_ROUTER not set in environment"); + } + const treasury = process.env.AGENT_TAX_TREASURY; + if (!treasury) { + throw new Error("AGENT_TAX_TREASURY not set in environment"); + } + const minSwapThreshold = process.env.TAX_MANAGER_MIN_SWAP_THRESHOLD; + if (!minSwapThreshold) { + throw new Error("TAX_MANAGER_MIN_SWAP_THRESHOLD not set in environment"); + } + const maxSwapThreshold = process.env.TAX_MANAGER_MAX_SWAP_THRESHOLD; + if (!maxSwapThreshold) { + throw new Error("TAX_MANAGER_MAX_SWAP_THRESHOLD not set in environment"); + } + + console.log("\nDeployment arguments loaded:", { + deployerAddress, + contractController, + admin, + assetToken, + taxToken, + uniswapV2Router, + treasury, + minSwapThreshold, + maxSwapThreshold, + }); + + const signers = await ethers.getSigners(); + const deployer = signers[0]; + console.log("Deployer address:", await deployer.getAddress()); + + // ============================================ + // 1. Deploy AgentDAO (implementation contract, not proxy) + // ============================================ + // console.log("\n--- Deploying AgentDAO (implementation) ---"); + // const AgentDAO = await ethers.getContractFactory("AgentDAO"); + // const agentDAO = await AgentDAO.deploy(); + // await agentDAO.waitForDeployment(); + // const agentDAOAddress = await agentDAO.getAddress(); + // console.log("AgentDAO (implementation) deployed at:", agentDAOAddress); + + // ============================================ + // 2. Deploy AgentNftV2 (upgradeable proxy) + // ============================================ + // console.log("\n--- Deploying AgentNftV2 (proxy) ---"); + // const AgentNftV2 = await ethers.getContractFactory("AgentNftV2"); + // const agentNftV2 = await upgrades.deployProxy( + // AgentNftV2, + // [admin], // initialize(address defaultAdmin) + // { + // initializer: "initialize", + // initialOwner: contractController, + // unsafeAllow: ["internal-function-storage"], + // } + // ); + // await agentNftV2.waitForDeployment(); + // const agentNftV2Address = await agentNftV2.getAddress(); + // console.log("AgentNftV2 (proxy) deployed at:", agentNftV2Address); + // Note: initialize() already grants DEFAULT_ADMIN_ROLE, VALIDATOR_ADMIN_ROLE, ADMIN_ROLE to admin + // MINTER_ROLE will be granted to AgentFactoryV6 in the main deployment script + + // ============================================ + // 3. Deploy AgentTax (AGENT_TOKEN_TAX_MANAGER) + // ============================================ + const agentNftV2Address = process.env.AGENT_NFT_V2; + + console.log("\n--- Deploying AgentTax (AGENT_TOKEN_TAX_MANAGER) ---"); + const AgentTax = await ethers.getContractFactory("AgentTax"); + const agentTax = await upgrades.deployProxy( + AgentTax, + [ + admin, // defaultAdmin_ + assetToken, // assetToken_ (BRIDGED_TOKEN/VIRTUAL) + taxToken, // taxToken_ + uniswapV2Router, // router_ + treasury, // treasury_ + parseEther(minSwapThreshold), // minSwapThreshold_ + parseEther(maxSwapThreshold), // maxSwapThreshold_ + agentNftV2Address, // nft_ (AgentNftV2) + ], + { + initializer: "initialize", + initialOwner: contractController, + } + ); + await agentTax.waitForDeployment(); + const agentTaxAddress = await agentTax.getAddress(); + console.log("AgentTax deployed at:", agentTaxAddress); + // Note: initialize() grants ADMIN_ROLE and DEFAULT_ADMIN_ROLE to admin + // Admin needs to manually grant EXECUTOR_ROLE and EXECUTOR_V2_ROLE to the executor address + + // ============================================ + // 4. Print Deployment Summary + // ============================================ + console.log("\n=== Prerequisites Deployment Summary ==="); + console.log("Copy the following addresses to your .env file:\n"); + // console.log(`AGENT_DAO=${agentDAOAddress}`); + console.log(`AGENT_NFT_V2=${agentNftV2Address}`); + console.log(`AGENT_TOKEN_TAX_MANAGER=${agentTaxAddress}`); + + console.log("\n--- Full Summary ---"); + // console.log("- AgentDAO (implementation):", agentDAOAddress); + console.log("- AgentNftV2 (proxy):", agentNftV2Address); + console.log("- AgentTax (AGENT_TOKEN_TAX_MANAGER):", agentTaxAddress); + + console.log("\n--- Manual Steps Required (by admin) ---"); + console.log("AgentNftV2: MINTER_ROLE will be granted to AgentFactoryV6 in main deployment script"); + console.log("AgentTax: Admin needs to grant EXECUTOR_ROLE and EXECUTOR_V2_ROLE to executor address"); + + console.log("\nPrerequisites deployment completed successfully!"); + } catch (e) { + console.error("Deployment failed:", e); + throw e; + } +})(); diff --git a/scripts/launchpadv5/deployLaunchpadv5_0.ts b/scripts/launchpadv5/deployLaunchpadv5_0.ts new file mode 100644 index 0000000..9741a60 --- /dev/null +++ b/scripts/launchpadv5/deployLaunchpadv5_0.ts @@ -0,0 +1,194 @@ +// npx hardhat run scripts/launchpadv5/deployLaunchpadv5_0.ts --network eth_sepolia +import { parseEther } from "ethers"; +const { ethers, upgrades } = require("hardhat"); + +/** + * Deploy prerequisites for FFactoryV2: + * - AgentNftV2 (optional, if not already deployed) + * - AgentTax (AGENT_TOKEN_TAX_MANAGER / FFactoryV2_TAX_VAULT) + * + * Run this script before deployLaunchpadv5_1.ts + */ +(async () => { + try { + console.log("\n" + "=".repeat(80)); + console.log(" Launchpad V5 - Step 0: Deploy Prerequisites (AgentTax)"); + console.log("=".repeat(80)); + + const [deployer] = await ethers.getSigners(); + const deployerAddress = await deployer.getAddress(); + console.log("Deployer address:", deployerAddress); + + // ============================================ + // Load required environment variables + // ============================================ + const contractController = process.env.CONTRACT_CONTROLLER; + if (!contractController) { + throw new Error("CONTRACT_CONTROLLER not set in environment"); + } + const admin = process.env.ADMIN; + if (!admin) { + throw new Error("ADMIN not set in environment"); + } + + // AgentTax parameters + const assetToken = process.env.AGENT_TAX_ASSET_TOKEN; + if (!assetToken) { + throw new Error("AGENT_TAX_ASSET_TOKEN not set in environment (used as assetToken for AgentTax)"); + } + + // Tax token - typically the same as asset token, or a separate tax token + const taxToken = process.env.AGENT_TAX_TAX_TOKEN; + if (!taxToken) { + throw new Error("AGENT_TAX_TAX_TOKEN not set in environment (used as taxToken for AgentTax)"); + } + + const agentTaxDexRouter = process.env.AGENT_TAX_DEX_ROUTER; + if (!agentTaxDexRouter) { + throw new Error("AGENT_TAX_DEX_ROUTER not set in environment"); + } + + const treasury = process.env.AGENT_TAX_TREASURY; + if (!treasury) { + throw new Error("AGENT_TAX_TREASURY not set in environment"); + } + + const minSwapThreshold = process.env.AGENT_TAX_MIN_SWAP_THRESHOLD; + if (!minSwapThreshold) { + throw new Error("AGENT_TAX_MIN_SWAP_THRESHOLD not set in environment"); + } + const maxSwapThreshold = process.env.AGENT_TAX_MAX_SWAP_THRESHOLD; + if (!maxSwapThreshold) { + throw new Error("AGENT_TAX_MAX_SWAP_THRESHOLD not set in environment"); + } + + // AgentNftV2 - required for AgentTax + let agentNftV2Address: string | undefined = process.env.AGENT_NFT_V2_ADDRESS; + + console.log("\nDeployment arguments loaded:", { + contractController, + admin, + assetToken, + taxToken, + agentTaxDexRouter, + treasury, + minSwapThreshold, + maxSwapThreshold, + agentNftV2Address: agentNftV2Address || "(will deploy)", + }); + + // Track deployed/reused contracts + const deployedContracts: { [key: string]: string } = {}; + const reusedContracts: { [key: string]: string } = {}; + + // ============================================ + // 1. Deploy AgentNftV2 (if not provided) + // ============================================ + if (!agentNftV2Address) { + console.log("\n--- Deploying AgentNftV2 (proxy) ---"); + const AgentNftV2 = await ethers.getContractFactory("AgentNftV2"); + // Initialize with deployer first so we can grant MINTER_ROLE later in _2.ts + const agentNftV2 = await upgrades.deployProxy( + AgentNftV2, + [deployerAddress], // initialize with deployer as defaultAdmin + { + initializer: "initialize", + initialOwner: contractController, + unsafeAllow: ["internal-function-storage"], + } + ); + await agentNftV2.waitForDeployment(); + agentNftV2Address = await agentNftV2.getAddress(); + console.log("AgentNftV2 (proxy) deployed at:", agentNftV2Address); + deployedContracts["AGENT_NFT_V2_ADDRESS"] = agentNftV2Address!; + + // Grant DEFAULT_ADMIN_ROLE to admin as well + const defaultAdminRole = await agentNftV2.DEFAULT_ADMIN_ROLE(); + const tx = await agentNftV2.grantRole(defaultAdminRole, admin); + await tx.wait(); + console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); + } else { + console.log("\n--- Using existing AgentNftV2 ---"); + console.log("AgentNftV2 address:", agentNftV2Address); + reusedContracts["AGENT_NFT_V2_ADDRESS"] = agentNftV2Address; + } + + // ============================================ + // 2. Deploy AgentTax (AGENT_TOKEN_TAX_MANAGER) + // ============================================ + const existingAgentTax = process.env.AGENT_TOKEN_TAX_MANAGER; + let agentTaxAddress: string; + + if (!existingAgentTax) { + console.log("\n--- Deploying AgentTax (AGENT_TOKEN_TAX_MANAGER) ---"); + const AgentTax = await ethers.getContractFactory("AgentTax"); + const agentTax = await upgrades.deployProxy( + AgentTax, + [ + admin, // defaultAdmin_ + assetToken, // assetToken_ + taxToken, // taxToken_ (VIRTUAL_TOKEN) + agentTaxDexRouter, // router_ + treasury, // treasury_ + minSwapThreshold, // minSwapThreshold_ + maxSwapThreshold, // maxSwapThreshold_ + agentNftV2Address, // nft_ (AgentNftV2) + ], + { + initializer: "initialize", + initialOwner: contractController, + } + ); + await agentTax.waitForDeployment(); + agentTaxAddress = await agentTax.getAddress(); + console.log("AgentTax deployed at:", agentTaxAddress); + deployedContracts["AGENT_TOKEN_TAX_MANAGER"] = agentTaxAddress; + } else { + console.log("\n--- Using existing AgentTax ---"); + console.log("AgentTax address:", existingAgentTax); + agentTaxAddress = existingAgentTax; + reusedContracts["AGENT_TOKEN_TAX_MANAGER"] = existingAgentTax; + } + + // ============================================ + // 3. Print Deployment Summary + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Deployment Summary"); + console.log("=".repeat(80)); + + if (Object.keys(deployedContracts).length > 0) { + console.log("\n--- Newly Deployed Contracts ---"); + for (const [name, address] of Object.entries(deployedContracts)) { + console.log(`${name}=${address}`); + } + } + + if (Object.keys(reusedContracts).length > 0) { + console.log("\n--- Reused Existing Contracts ---"); + for (const [name, address] of Object.entries(reusedContracts)) { + console.log(`${name}=${address}`); + } + } + + console.log("\n--- Environment Variables for .env file ---"); + console.log("# Prerequisites for FFactoryV2:"); + console.log(`AGENT_NFT_V2_ADDRESS=${agentNftV2Address}`); + console.log(`AGENT_TOKEN_TAX_MANAGER=${agentTaxAddress}`); + console.log(`FFactoryV2_TAX_VAULT=${agentTaxAddress}`); + + console.log("\n--- Manual Steps Required (by admin) ---"); + console.log("1. AgentNftV2: MINTER_ROLE will be granted to AgentFactoryV6 in deployLaunchpadv5_2.ts"); + console.log("2. AgentTax: Admin may need to grant EXECUTOR_ROLE and EXECUTOR_V2_ROLE to executor address"); + + console.log("\n--- Next Step ---"); + console.log("Run: npx hardhat run scripts/launchpadv5/deployLaunchpadv5_1.ts --network "); + + console.log("\n" + "=".repeat(80)); + console.log(" Step 0 Completed Successfully!"); + console.log("=".repeat(80)); + } catch (e) { + console.error("❌ Deployment failed:", e); + process.exit(1); + } +})(); diff --git a/scripts/launchpadv5/deployLaunchpadv5_1.ts b/scripts/launchpadv5/deployLaunchpadv5_1.ts new file mode 100644 index 0000000..80b6837 --- /dev/null +++ b/scripts/launchpadv5/deployLaunchpadv5_1.ts @@ -0,0 +1,250 @@ +import { parseEther } from "ethers"; +const { ethers, upgrades } = require("hardhat"); + +// Environment variables are loaded via hardhat.config.js +// Make sure hardhat.config.js has: require("dotenv").config({ path: ".env.launchpadv5_dev" }); + +/** + * Deploy FFactoryV2 and FRouterV2 + * Run this script first, then use the output addresses in deployPrerequisitesV5.ts + */ +(async () => { + try { + console.log("\n=== FFactoryV2 & FRouterV2 Deployment Starting ==="); + + const [deployer] = await ethers.getSigners(); + const deployerAddress = await deployer.getAddress(); + console.log("Deployer address:", deployerAddress); + + // ============================================ + // Load required environment variables + // ============================================ + const contractController = process.env.CONTRACT_CONTROLLER; + if (!contractController) { + throw new Error("CONTRACT_CONTROLLER not set in environment"); + } + const admin = process.env.ADMIN; + if (!admin) { + throw new Error("ADMIN not set in environment"); + } + const virtualTokenAddress = process.env.VIRTUAL_TOKEN_ADDRESS; + if (!virtualTokenAddress) { + throw new Error("VIRTUAL_TOKEN_ADDRESS not set in environment"); + } + + // FFactoryV2 parameters (Prototype phase - bonding curve) + const taxVault = process.env.FFactoryV2_TAX_VAULT; + if (!taxVault) { + throw new Error("FFactoryV2_TAX_VAULT not set in environment"); + } + const antiSniperTaxVault = process.env.FFactoryV2_ANTI_SNIPER_TAX_VAULT; + if (!antiSniperTaxVault) { + throw new Error( + "FFactoryV2_ANTI_SNIPER_TAX_VAULT not set in environment" + ); + } + const prototypeBuyTax = process.env.PROTOTYPE_BUY_TAX; + if (!prototypeBuyTax) { + throw new Error("PROTOTYPE_BUY_TAX not set in environment"); + } + const prototypeSellTax = process.env.PROTOTYPE_SELL_TAX; + if (!prototypeSellTax) { + throw new Error("PROTOTYPE_SELL_TAX not set in environment"); + } + const antiSniperBuyTaxStartValue = + process.env.ANTI_SNIPER_BUY_TAX_START_VALUE; + if (!antiSniperBuyTaxStartValue) { + throw new Error("ANTI_SNIPER_BUY_TAX_START_VALUE not set in environment"); + } + const beOpsWallet = process.env.BE_OPS_WALLET; + if (!beOpsWallet) { + throw new Error("BE_OPS_WALLET not set in environment"); + } + + console.log("\nDeployment arguments loaded:", { + contractController, + admin, + virtualTokenAddress, + taxVault, + antiSniperTaxVault, + prototypeBuyTax, + beOpsWallet, + prototypeSellTax, + antiSniperBuyTaxStartValue, + }); + + // Track deployed/reused contracts + const deployedContracts: { [key: string]: string } = {}; + const reusedContracts: { [key: string]: string } = {}; + + // Check if contracts already exist + const existingFFactoryV2 = process.env.FFactoryV2_ADDRESS; + const existingFRouterV2 = process.env.FRouterV2_ADDRESS; + + // ============================================ + // 1. Deploy or reuse FFactoryV2 + // ============================================ + let fFactoryV2Address: string; + let fFactoryV2: any; + + if (existingFFactoryV2) { + console.log("\n--- FFactoryV2 already exists, skipping deployment ---"); + fFactoryV2Address = existingFFactoryV2; + fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address); + reusedContracts.FFactoryV2 = fFactoryV2Address; + console.log("Using existing FFactoryV2 at:", fFactoryV2Address); + } else { + console.log("\n--- Deploying FFactoryV2 ---"); + const FFactoryV2Factory = await ethers.getContractFactory("FFactoryV2"); + fFactoryV2 = await upgrades.deployProxy( + FFactoryV2Factory, + [ + taxVault, + prototypeBuyTax, + prototypeSellTax, + antiSniperBuyTaxStartValue, + antiSniperTaxVault, + ], + { + initializer: "initialize", + initialOwner: contractController, + } + ); + await fFactoryV2.waitForDeployment(); + fFactoryV2Address = await fFactoryV2.getAddress(); + deployedContracts.FFactoryV2 = fFactoryV2Address; + console.log("FFactoryV2 deployed at:", fFactoryV2Address); + } + + // ============================================ + // 2. Deploy or reuse FRouterV2 + // ============================================ + let fRouterV2Address: string; + let fRouterV2: any; + + if (existingFRouterV2) { + console.log("\n--- FRouterV2 already exists, skipping deployment ---"); + fRouterV2Address = existingFRouterV2; + fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address); + reusedContracts.FRouterV2 = fRouterV2Address; + console.log("Using existing FRouterV2 at:", fRouterV2Address); + } else { + console.log("\n--- Deploying FRouterV2 ---"); + const FRouterV2Factory = await ethers.getContractFactory("FRouterV2"); + fRouterV2 = await upgrades.deployProxy( + FRouterV2Factory, + [fFactoryV2Address, virtualTokenAddress], + { + initializer: "initialize", + initialOwner: contractController, + } + ); + await fRouterV2.waitForDeployment(); + fRouterV2Address = await fRouterV2.getAddress(); + deployedContracts.FRouterV2 = fRouterV2Address; + console.log("FRouterV2 deployed at:", fRouterV2Address); + } + + // If both contracts were reused, skip configuration + if (existingFFactoryV2 && existingFRouterV2) { + console.log("\n=== Both contracts already exist, skipping configuration ==="); + console.log("FFactoryV2:", fFactoryV2Address); + console.log("FRouterV2:", fRouterV2Address); + console.log("\nNo changes made. Proceed to next deployment step."); + return; + } + + // ============================================ + // 3. Configure FFactoryV2 (only if newly deployed) + // ============================================ + if (!existingFFactoryV2) { + console.log("\n--- Configuring FFactoryV2 ---"); + const adminRole = await fFactoryV2.ADMIN_ROLE(); + const defaultAdminRole = await fFactoryV2.DEFAULT_ADMIN_ROLE(); + + // Grant ADMIN_ROLE to deployer temporarily + await fFactoryV2.grantRole(adminRole, deployerAddress); + console.log("ADMIN_ROLE granted to deployer temporarily"); + + // Set Router + await fFactoryV2.setRouter(fRouterV2Address); + console.log("Router set in FFactoryV2"); + + // Grant roles to admin + await fFactoryV2.grantRole(adminRole, admin); + console.log("ADMIN_ROLE granted to admin:", admin); + + await fFactoryV2.grantRole(defaultAdminRole, admin); + console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); + } + + // ============================================ + // 4. Configure FRouterV2 (only if newly deployed) + // ============================================ + if (!existingFRouterV2) { + console.log("\n--- Configuring FRouterV2 ---"); + + // Grant ADMIN_ROLE to admin (needed for setBondingV5) + await fRouterV2.grantRole(await fRouterV2.ADMIN_ROLE(), admin); + console.log("ADMIN_ROLE granted to admin on FRouterV2"); + + // Grant DEFAULT_ADMIN_ROLE to admin + await fRouterV2.grantRole(await fRouterV2.DEFAULT_ADMIN_ROLE(), admin); + console.log("DEFAULT_ADMIN_ROLE granted to admin on FRouterV2"); + + // Grant EXECUTOR_ROLE to BE_OPS_WALLET (for resetTime) + const executorRole = await fRouterV2.EXECUTOR_ROLE(); + await fRouterV2.grantRole(executorRole, beOpsWallet); + console.log("EXECUTOR_ROLE granted to BE_OPS_WALLET:", beOpsWallet); + } + + // NOTE: Deployer roles are NOT revoked here + // Run deployRevokeRoles.ts after all deployments are complete + + // ============================================ + // 5. Print Deployment Summary + // ============================================ + console.log("\n=== FFactoryV2 & FRouterV2 Deployment Summary ==="); + + if (Object.keys(deployedContracts).length > 0) { + console.log("\n--- Newly Deployed Contracts ---"); + for (const [name, address] of Object.entries(deployedContracts)) { + console.log(`- ${name}: ${address}`); + } + console.log("\nCopy the following to your .env file:"); + for (const [name, address] of Object.entries(deployedContracts)) { + console.log(`${name}_ADDRESS=${address}`); + } + } + + if (Object.keys(reusedContracts).length > 0) { + console.log("\n--- Reused Contracts (already existed) ---"); + for (const [name, address] of Object.entries(reusedContracts)) { + console.log(`- ${name}: ${address}`); + } + } + + console.log("\n--- Final Contract Addresses ---"); + console.log(`FFactoryV2_ADDRESS=${fFactoryV2Address}`); + console.log(`FRouterV2_ADDRESS=${fRouterV2Address}`); + + console.log("\n--- Deployment Order ---"); + console.log("1. ✅ deployLaunchpadv5_1.ts (FFactoryV2, FRouterV2) - DONE"); + console.log("2. ⏳ deployLaunchpadv5_2.ts (AgentFactoryV6)"); + console.log("3. ⏳ deployLaunchpadv5_3.ts (BondingConfig, BondingV5)"); + console.log("4. ⏳ deployLaunchpadv5_4.ts (Revoke deployer roles)"); + + console.log("\n--- Next Step ---"); + console.log( + "1. Add FFactoryV2_ADDRESS and FRouterV2_ADDRESS to your .env file" + ); + console.log( + "2. Run: npx hardhat run scripts/launchpadv5/deployLaunchpadv5_2.ts --network " + ); + + console.log("\nDeployment completed successfully!"); + } catch (e) { + console.error("Deployment failed:", e); + throw e; + } +})(); diff --git a/scripts/launchpadv5/deployLaunchpadv5_2.ts b/scripts/launchpadv5/deployLaunchpadv5_2.ts new file mode 100644 index 0000000..324dc5f --- /dev/null +++ b/scripts/launchpadv5/deployLaunchpadv5_2.ts @@ -0,0 +1,393 @@ +import { parseEther } from "ethers"; +const { ethers, upgrades } = require("hardhat"); + +// Environment variables are loaded via hardhat.config.js +// Make sure hardhat.config.js has: require("dotenv").config({ path: ".env.launchpadv5_dev" }); + +/** + * Deploy AgentFactoryV6 and dependencies + * Prerequisites: FFactoryV2 and FRouterV2 must already be deployed (run deployPrerequisites_1.ts first) + */ +(async () => { + try { + console.log("\n=== AgentFactoryV6 Deployment Starting ==="); + console.log( + "Prerequisites: FFactoryV2 and FRouterV2 must already be deployed" + ); + + // Check if AgentFactoryV6 already exists + const existingAgentFactoryV6 = process.env.AGENT_FACTORY_V6_ADDRESS; + if (existingAgentFactoryV6) { + console.log("\n=== AgentFactoryV6 already exists, skipping deployment ==="); + console.log("AGENT_FACTORY_V6_ADDRESS:", existingAgentFactoryV6); + console.log("\nNo changes made. Proceed to next deployment step:"); + console.log("Run: npx hardhat run scripts/launchpadv5/deployLaunchpadv5_3.ts --network "); + return; + } + + const [deployer] = await ethers.getSigners(); + const deployerAddress = await deployer.getAddress(); + console.log("Deployer address:", deployerAddress); + + // ============================================ + // Load required environment variables + // ============================================ + const contractController = process.env.CONTRACT_CONTROLLER; + if (!contractController) { + throw new Error("CONTRACT_CONTROLLER not set in environment"); + } + const admin = process.env.ADMIN; + if (!admin) { + throw new Error("ADMIN not set in environment"); + } + const virtualTokenAddress = process.env.VIRTUAL_TOKEN_ADDRESS; + if (!virtualTokenAddress) { + throw new Error("VIRTUAL_TOKEN_ADDRESS not set in environment"); + } + + // FFactoryV2 and FRouterV2 addresses (from deployPrerequisites_1.ts) + const fFactoryV2Address = process.env.FFactoryV2_ADDRESS; + if (!fFactoryV2Address) { + throw new Error( + "FFactoryV2_ADDRESS not set - run deployPrerequisites_1.ts first" + ); + } + const fRouterV2Address = process.env.FRouterV2_ADDRESS; + if (!fRouterV2Address) { + throw new Error( + "FRouterV2_ADDRESS not set - run deployPrerequisites_1.ts first" + ); + } + + // AgentFactoryV6 tax params (Sentient phase - different from FFactoryV2 Prototype phase) + const sentientBuyTax = process.env.SENTIENT_BUY_TAX; + if (!sentientBuyTax) { + throw new Error("SENTIENT_BUY_TAX not set in environment"); + } + const sentientSellTax = process.env.SENTIENT_SELL_TAX; + if (!sentientSellTax) { + throw new Error("SENTIENT_SELL_TAX not set in environment"); + } + const agentTokenTaxManager = process.env.AGENT_TOKEN_TAX_MANAGER; + if (!agentTokenTaxManager) { + throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + } + + // REQUIRED: AgentNftV2 must be deployed first (in deployLaunchpadv5_0.ts) + const agentNftV2Address = process.env.AGENT_NFT_V2_ADDRESS; + if (!agentNftV2Address) { + throw new Error("AGENT_NFT_V2_ADDRESS not set - run deployLaunchpadv5_0.ts first"); + } + + // AgentFactoryV6 parameters (optional - will deploy if not provided) + const agentTokenV2Impl = process.env.AGENT_TOKEN_V2_IMPLEMENTATION; + const agentVeTokenV2Impl = process.env.AGENT_VE_TOKEN_V2_IMPLEMENTATION; + const agentDAOImpl = process.env.AGENT_DAO_IMPLEMENTATION; + + // Required external contract addresses + const tbaRegistry = process.env.TBA_REGISTRY; + if (!tbaRegistry) { + throw new Error("TBA_REGISTRY not set in environment"); + } + const uniswapV2RouterAddress = process.env.UNISWAP_V2_ROUTER; + if (!uniswapV2RouterAddress) { + throw new Error("UNISWAP_V2_ROUTER not set in environment"); + } + + // AgentFactoryV6 config parameters + const agentFactoryV6Vault = process.env.AGENT_FACTORY_V6_VAULT; + if (!agentFactoryV6Vault) { + throw new Error("AGENT_FACTORY_V6_VAULT not set in environment"); + } + const agentFactoryV6NextId = process.env.AGENT_FACTORY_V6_NEXT_ID; + if (!agentFactoryV6NextId) { + throw new Error("AGENT_FACTORY_V6_NEXT_ID not set in environment"); + } + const agentFactoryV6MaturityDuration = + process.env.AGENT_FACTORY_V6_MATURITY_DURATION; + if (!agentFactoryV6MaturityDuration) { + throw new Error( + "AGENT_FACTORY_V6_MATURITY_DURATION not set in environment" + ); + } + const taxSwapThresholdBasisPoints = + process.env.AGENT_FACTORY_V6_TAX_SWAP_THRESHOLD_BASIS_POINTS; + if (!taxSwapThresholdBasisPoints) { + throw new Error( + "AGENT_FACTORY_V6_TAX_SWAP_THRESHOLD_BASIS_POINTS not set in environment" + ); + } + + console.log("\nDeployment arguments loaded:", { + contractController, + admin, + virtualTokenAddress, + fFactoryV2Address, + fRouterV2Address, + sentientBuyTax, + sentientSellTax, + agentTokenTaxManager, + agentTokenV2Impl: agentTokenV2Impl || "(will deploy)", + agentVeTokenV2Impl: agentVeTokenV2Impl || "(will deploy)", + agentDAOImpl: agentDAOImpl || "(will deploy)", + tbaRegistry, + agentNftV2Address, + uniswapV2RouterAddress, + agentFactoryV6Vault, + agentFactoryV6NextId, + agentFactoryV6MaturityDuration, + taxSwapThresholdBasisPoints, + }); + + // Track deployed contracts + const deployedContracts: { [key: string]: string } = {}; + + // ============================================ + // 1. Deploy AgentTokenV2 implementation + // ============================================ + let agentTokenV2ImplAddress: string; + if (!agentTokenV2Impl) { + console.log("\n--- Deploying AgentTokenV2 implementation ---"); + const AgentTokenV2 = await ethers.getContractFactory("AgentTokenV2"); + const agentTokenV2 = await AgentTokenV2.deploy(); + await agentTokenV2.waitForDeployment(); + agentTokenV2ImplAddress = await agentTokenV2.getAddress(); + deployedContracts.AgentTokenV2Impl = agentTokenV2ImplAddress; + console.log( + "AgentTokenV2 implementation deployed at:", + agentTokenV2ImplAddress + ); + } else { + agentTokenV2ImplAddress = agentTokenV2Impl; + console.log( + "\n--- Reusing AgentTokenV2 implementation:", + agentTokenV2ImplAddress, + "---" + ); + } + + // ============================================ + // 2. Deploy AgentVeTokenV2 implementation + // ============================================ + let agentVeTokenV2ImplAddress: string; + if (!agentVeTokenV2Impl) { + console.log("\n--- Deploying AgentVeTokenV2 implementation ---"); + const AgentVeTokenV2 = await ethers.getContractFactory("AgentVeTokenV2"); + const agentVeTokenV2 = await AgentVeTokenV2.deploy(); + await agentVeTokenV2.waitForDeployment(); + agentVeTokenV2ImplAddress = await agentVeTokenV2.getAddress(); + deployedContracts.AgentVeTokenV2Impl = agentVeTokenV2ImplAddress; + console.log( + "AgentVeTokenV2 implementation deployed at:", + agentVeTokenV2ImplAddress + ); + } else { + agentVeTokenV2ImplAddress = agentVeTokenV2Impl; + console.log( + "\n--- Reusing AgentVeTokenV2 implementation:", + agentVeTokenV2ImplAddress, + "---" + ); + } + + // ============================================ + // 3. Deploy AgentDAO implementation (if not provided) + // ============================================ + let agentDAOImplAddress: string; + if (!agentDAOImpl) { + console.log("\n--- Deploying AgentDAO implementation ---"); + const AgentDAO = await ethers.getContractFactory("AgentDAO"); + const agentDAO = await AgentDAO.deploy(); + await agentDAO.waitForDeployment(); + agentDAOImplAddress = await agentDAO.getAddress(); + deployedContracts.AgentDAOImpl = agentDAOImplAddress; + console.log("AgentDAO implementation deployed at:", agentDAOImplAddress); + } else { + agentDAOImplAddress = agentDAOImpl; + console.log( + "\n--- Reusing AgentDAO implementation:", + agentDAOImplAddress, + "---" + ); + } + + // ============================================ + // 4. Use provided TBA Registry (canonical ERC-6551 Registry) + // ============================================ + console.log("--- Using TBA Registry:", tbaRegistry, "---"); + + // ============================================ + // 5. Use provided AgentNftV2 (deployed in deployLaunchpadv5_0.ts) + // ============================================ + console.log("--- Using AgentNftV2:", agentNftV2Address, "---"); + + // ============================================ + // 6. Use provided UniswapV2Router + // ============================================ + console.log("--- Using UniswapV2Router:", uniswapV2RouterAddress, "---"); + + // ============================================ + // 7. Deploy AgentFactoryV6 + // ============================================ + console.log("\n--- Deploying AgentFactoryV6 ---"); + const AgentFactoryV6 = await ethers.getContractFactory("AgentFactoryV6"); + const agentFactoryV6 = await upgrades.deployProxy( + AgentFactoryV6, + [ + agentTokenV2ImplAddress, // tokenImplementation_ + agentVeTokenV2ImplAddress, // veTokenImplementation_ + agentDAOImplAddress, // daoImplementation_ + tbaRegistry, // tbaRegistry_ + virtualTokenAddress, // assetToken_ + agentNftV2Address, // nft_ (deployed in deployLaunchpadv5_0.ts) + agentFactoryV6Vault, // vault_ + agentFactoryV6NextId, // nextId_ + ], + { + initializer: "initialize", + initialOwner: contractController, + } + ); + await agentFactoryV6.waitForDeployment(); + const agentFactoryV6Address = await agentFactoryV6.getAddress(); + deployedContracts.AgentFactoryV6 = agentFactoryV6Address; + console.log("AgentFactoryV6 deployed at:", agentFactoryV6Address); + + // ============================================ + // 8. Configure AgentFactoryV6 + // ============================================ + console.log("\n--- Configuring AgentFactoryV6 ---"); + + // Grant DEFAULT_ADMIN_ROLE to deployer temporarily + await agentFactoryV6.grantRole( + await agentFactoryV6.DEFAULT_ADMIN_ROLE(), + deployerAddress + ); + console.log("DEFAULT_ADMIN_ROLE granted to deployer temporarily"); + + // Set params + await agentFactoryV6.setParams( + agentFactoryV6MaturityDuration, // maturity duration (from env) + uniswapV2RouterAddress, + admin, // defaultDelegatee + admin // tokenAdmin + ); + console.log( + "setParams() called for AgentFactoryV6 with maturityDuration:", + agentFactoryV6MaturityDuration + ); + + // Set token params (Sentient phase tax configuration) + await agentFactoryV6.setTokenParams( + sentientBuyTax, // projectBuyTaxBasisPoints + sentientSellTax, // projectSellTaxBasisPoints + taxSwapThresholdBasisPoints, // taxSwapThresholdBasisPoints (from env) + agentTokenTaxManager // projectTaxRecipient (fee address) + ); + console.log("setTokenParams() called for AgentFactoryV6:", { + sentientBuyTax, + sentientSellTax, + taxSwapThresholdBasisPoints, + agentTokenTaxManager, + }); + + // Grant DEFAULT_ADMIN_ROLE to admin + await agentFactoryV6.grantRole( + await agentFactoryV6.DEFAULT_ADMIN_ROLE(), + admin + ); + console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); + + // Grant REMOVE_LIQUIDITY_ROLE to admin + const removeLiqRole = await agentFactoryV6.REMOVE_LIQUIDITY_ROLE(); + await agentFactoryV6.grantRole(removeLiqRole, admin); + console.log("REMOVE_LIQUIDITY_ROLE granted to admin:", admin); + + // ============================================ + // 9. Grant MINTER_ROLE on AgentNftV2 to AgentFactoryV6 + // ============================================ + // AgentNftV2 was deployed in deployLaunchpadv5_0.ts with deployer having DEFAULT_ADMIN_ROLE + console.log("\n--- Configuring AgentNftV2 roles ---"); + const agentNftV2Contract = await ethers.getContractAt( + "AgentNftV2", + agentNftV2Address + ); + + // Check if deployer has admin role to grant MINTER_ROLE + const defaultAdminRole = await agentNftV2Contract.DEFAULT_ADMIN_ROLE(); + const hasAdminRole = await agentNftV2Contract.hasRole(defaultAdminRole, deployerAddress); + + if (hasAdminRole) { + // Grant MINTER_ROLE to AgentFactoryV6 + const minterRole = await agentNftV2Contract.MINTER_ROLE(); + const tx = await agentNftV2Contract.grantRole(minterRole, agentFactoryV6Address); + await tx.wait(); + console.log("MINTER_ROLE granted to AgentFactoryV6:", agentFactoryV6Address); + } else { + console.log( + "\n⚠️ Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentNftV2" + ); + console.log(" Admin needs to grant MINTER_ROLE manually:"); + console.log( + ` AgentNftV2.grantRole(MINTER_ROLE, ${agentFactoryV6Address})` + ); + throw new Error("Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentNftV2, need manually do"); + } + + // NOTE: Deployer roles are NOT revoked here + // Run deployRevokeRoles.ts after all deployments are complete + + // ============================================ + // 10. Print Deployment Summary + // ============================================ + console.log("\n=== AgentFactoryV6 Deployment Summary ==="); + console.log("Copy the following to your .env file:\n"); + console.log(`AGENT_FACTORY_V6_ADDRESS=${agentFactoryV6Address}`); + if (deployedContracts.AgentTokenV2Impl) { + console.log(`AGENT_TOKEN_V2_IMPLEMENTATION=${agentTokenV2ImplAddress}`); + } + if (deployedContracts.AgentVeTokenV2Impl) { + console.log( + `AGENT_VE_TOKEN_V2_IMPLEMENTATION=${agentVeTokenV2ImplAddress}` + ); + } + if (deployedContracts.AgentDAOImpl) { + console.log(`AGENT_DAO_IMPLEMENTATION=${agentDAOImplAddress}`); + } + + console.log("\n--- Prerequisites (already deployed) ---"); + console.log(`- FFactoryV2: ${fFactoryV2Address}`); + console.log(`- FRouterV2: ${fRouterV2Address}`); + console.log(`- AgentNftV2: ${agentNftV2Address}`); + + console.log("\n--- Newly Deployed Contracts ---"); + for (const [name, address] of Object.entries(deployedContracts)) { + console.log(`- ${name}: ${address}`); + } + + console.log("\n--- Manual Steps Required (by admin) ---"); + if (!hasAdminRole) { + console.log( + `- Grant MINTER_ROLE on AgentNftV2 to AgentFactoryV6: ${agentFactoryV6Address}` + ); + } + + console.log("\n--- Deployment Order ---"); + console.log("0. ✅ deployLaunchpadv5_0.ts (AgentTax, AgentNftV2) - DONE"); + console.log("1. ✅ deployLaunchpadv5_1.ts (FFactoryV2, FRouterV2) - DONE"); + console.log("2. ✅ deployLaunchpadv5_2.ts (AgentFactoryV6) - DONE"); + console.log("3. ⏳ deployLaunchpadv5_3.ts (BondingConfig, BondingV5)"); + console.log("4. ⏳ deployLaunchpadv5_4.ts (Revoke deployer roles)"); + + console.log("\n--- Next Step ---"); + console.log("1. Add AGENT_FACTORY_V6_ADDRESS to your .env file"); + console.log( + "2. Run: npx hardhat run scripts/launchpadv5/deployLaunchpadv5_3.ts --network " + ); + + console.log("\nDeployment completed successfully!"); + } catch (e) { + console.error("Deployment failed:", e); + throw e; + } +})(); diff --git a/scripts/launchpadv5/deployLaunchpadv5_3.ts b/scripts/launchpadv5/deployLaunchpadv5_3.ts new file mode 100644 index 0000000..d64c729 --- /dev/null +++ b/scripts/launchpadv5/deployLaunchpadv5_3.ts @@ -0,0 +1,317 @@ +import { parseEther } from "ethers"; +const { ethers, upgrades } = require("hardhat"); + +// Environment variables are loaded via hardhat.config.js +// Make sure hardhat.config.js has: require("dotenv").config({ path: ".env.launchpadv5_dev" }); + +(async () => { + try { + console.log("\n=== LaunchpadV5 Deployment Starting ==="); + console.log("This script deploys: BondingConfig, BondingV5"); + console.log("Prerequisites (FFactoryV2, FRouterV2, AgentFactoryV6) must already exist."); + + const [deployer] = await ethers.getSigners(); + const deployerAddress = await deployer.getAddress(); + console.log("Deployer address:", deployerAddress); + + // ============================================ + // Load required environment variables + // ============================================ + const contractController = process.env.CONTRACT_CONTROLLER; + if (!contractController) { + throw new Error("CONTRACT_CONTROLLER not set in environment"); + } + + // Prerequisite contract addresses (required) + const fFactoryV2Address = process.env.FFactoryV2_ADDRESS; + if (!fFactoryV2Address) { + throw new Error("FFactoryV2_ADDRESS not set - run deployPrerequisitesV5.ts first"); + } + const fRouterV2Address = process.env.FRouterV2_ADDRESS; + if (!fRouterV2Address) { + throw new Error("FRouterV2_ADDRESS not set - run deployPrerequisitesV5.ts first"); + } + const agentFactoryV6Address = process.env.AGENT_FACTORY_V6_ADDRESS; + if (!agentFactoryV6Address) { + throw new Error("AGENT_FACTORY_V6_ADDRESS not set - run deployPrerequisitesV5.ts first"); + } + + // BondingConfig parameters + const creationFeeToAddress = process.env.LAUNCHPAD_V5_CREATION_FEE_TO_ADDRESS; + if (!creationFeeToAddress) { + throw new Error("LAUNCHPAD_V5_CREATION_FEE_TO_ADDRESS not set in environment"); + } + const initialSupply = process.env.INITIAL_SUPPLY; + if (!initialSupply) { + throw new Error("INITIAL_SUPPLY not set in environment"); + } + const startTimeDelay = process.env.LAUNCHPAD_V5_START_TIME_DELAY; + if (!startTimeDelay) { + throw new Error("LAUNCHPAD_V5_START_TIME_DELAY not set in environment"); + } + const normalLaunchFee = process.env.LAUNCHPAD_V5_NORMAL_LAUNCH_FEE; + if (!normalLaunchFee) { + throw new Error("LAUNCHPAD_V5_NORMAL_LAUNCH_FEE not set in environment"); + } + const acfFee = process.env.LAUNCHPAD_V5_ACF_FEE; + if (!acfFee) { + throw new Error("LAUNCHPAD_V5_ACF_FEE not set in environment"); + } + const tbaSalt = process.env.TBA_SALT; + if (!tbaSalt) { + throw new Error("TBA_SALT not set in environment"); + } + const tbaImplementation = process.env.TBA_IMPLEMENTATION; + if (!tbaImplementation) { + throw new Error("TBA_IMPLEMENTATION not set in environment"); + } + const daoVotingPeriod = process.env.DAO_VOTING_PERIOD; + if (!daoVotingPeriod) { + throw new Error("DAO_VOTING_PERIOD not set in environment"); + } + const daoThreshold = process.env.DAO_THRESHOLD; + if (!daoThreshold) { + throw new Error("DAO_THRESHOLD not set in environment"); + } + const teamTokenReservedWallet = process.env.TEAM_TOKEN_RESERVED_WALLET; + if (!teamTokenReservedWallet) { + throw new Error("TEAM_TOKEN_RESERVED_WALLET not set in environment"); + } + const fakeInitialVirtualLiq = process.env.FAKE_INITIAL_VIRTUAL_LIQ; + if (!fakeInitialVirtualLiq) { + throw new Error("FAKE_INITIAL_VIRTUAL_LIQ not set in environment"); + } + const targetRealVirtual = process.env.TARGET_REAL_VIRTUAL; + if (!targetRealVirtual) { + throw new Error("TARGET_REAL_VIRTUAL not set in environment"); + } + const maxAirdropPercent = process.env.MAX_AIRDROP_PERCENT || "5"; + + console.log("\nDeployment arguments loaded:", { + contractController, + fFactoryV2Address, + fRouterV2Address, + agentFactoryV6Address, + creationFeeToAddress, + initialSupply, + startTimeDelay, + normalLaunchFee, + acfFee, + tbaSalt, + tbaImplementation, + daoVotingPeriod, + daoThreshold, + teamTokenReservedWallet, + fakeInitialVirtualLiq, + targetRealVirtual, + maxAirdropPercent, + }); + + // ============================================ + // 1. Deploy BondingConfig + // ============================================ + console.log("\n--- Deploying BondingConfig ---"); + const BondingConfig = await ethers.getContractFactory("BondingConfig"); + + const scheduledLaunchParams = { + startTimeDelay: startTimeDelay, + normalLaunchFee: parseEther(normalLaunchFee).toString(), + acfFee: parseEther(acfFee).toString(), + }; + + const deployParams = { + tbaSalt: tbaSalt, + tbaImplementation: tbaImplementation, + daoVotingPeriod: daoVotingPeriod, + daoThreshold: daoThreshold, + }; + + const bondingCurveParams = { + fakeInitialVirtualLiq: parseEther(fakeInitialVirtualLiq).toString(), + targetRealVirtual: parseEther(targetRealVirtual).toString(), + }; + + const bondingConfig = await upgrades.deployProxy( + BondingConfig, + [ + initialSupply, + creationFeeToAddress, + teamTokenReservedWallet, + maxAirdropPercent, + scheduledLaunchParams, + deployParams, + bondingCurveParams, + ], + { + initializer: "initialize", + initialOwner: contractController, + } + ); + await bondingConfig.waitForDeployment(); + const bondingConfigAddress = await bondingConfig.getAddress(); + console.log("BondingConfig deployed at:", bondingConfigAddress); + + // Set X Launchers + const xLauncherAddresses = process.env.X_LAUNCHER_ADDRESSES; + if (xLauncherAddresses) { + const addresses = xLauncherAddresses.split(",").map((addr) => addr.trim()).filter((addr) => addr); + console.log("\n--- Setting X Launchers ---"); + for (const addr of addresses) { + const tx = await bondingConfig.setXLauncher(addr, true); + await tx.wait(); + console.log(`Set X Launcher: ${addr}`); + } + } + + // Set ACP Skill Launchers + const acpSkillLauncherAddresses = process.env.ACP_SKILL_LAUNCHER_ADDRESSES; + if (acpSkillLauncherAddresses) { + const addresses = acpSkillLauncherAddresses.split(",").map((addr) => addr.trim()).filter((addr) => addr); + console.log("\n--- Setting ACP Skill Launchers ---"); + for (const addr of addresses) { + const tx = await bondingConfig.setAcpSkillLauncher(addr, true); + await tx.wait(); + console.log(`Set ACP Skill Launcher: ${addr}`); + } + } + + // ============================================ + // 2. Deploy BondingV5 + // ============================================ + console.log("\n--- Deploying BondingV5 ---"); + const BondingV5 = await ethers.getContractFactory("BondingV5"); + const bondingV5 = await upgrades.deployProxy( + BondingV5, + [ + fFactoryV2Address, + fRouterV2Address, + agentFactoryV6Address, + bondingConfigAddress, + ], + { + initializer: "initialize", + initialOwner: contractController, + } + ); + await bondingV5.waitForDeployment(); + const bondingV5Address = await bondingV5.getAddress(); + console.log("BondingV5 deployed at:", bondingV5Address); + + // ============================================ + // 3. Transfer Ownership + // ============================================ + console.log("\n--- Transferring ownership to CONTRACT_CONTROLLER ---"); + + const tx5 = await bondingV5.transferOwnership(contractController); + await tx5.wait(); + console.log("BondingV5 ownership transferred to CONTRACT_CONTROLLER"); + + const tx6 = await bondingConfig.transferOwnership(contractController); + await tx6.wait(); + console.log("BondingConfig ownership transferred to CONTRACT_CONTROLLER"); + + // ============================================ + // 4. Grant Roles and Configure Contracts (using admin wallet) + // ============================================ + // These contracts were deployed in previous scripts, so deployer no longer has admin roles + // We need to use ADMIN_PRIVATE_KEY to grant roles + console.log("\n--- Granting necessary roles (using admin wallet) ---"); + + const adminPrivateKey = process.env.ADMIN_PRIVATE_KEY; + if (!adminPrivateKey) { + console.log("\n" + "=".repeat(80)); + console.log("⚠️ ADMIN_PRIVATE_KEY not set - Manual role grants required!"); + console.log("⚠️ The following operations must be done manually by admin:"); + console.log("=".repeat(80)); + console.log(`1. FFactoryV2.grantRole(CREATOR_ROLE, ${bondingV5Address})`); + console.log(`2. FRouterV2.setBondingV5(${bondingV5Address}, ${bondingConfigAddress})`); + console.log(`3. FRouterV2.grantRole(EXECUTOR_ROLE, ${bondingV5Address})`); + console.log(`4. AgentFactoryV6.grantRole(BONDING_ROLE, ${bondingV5Address})`); + console.log("=".repeat(80)); + throw new Error("ADMIN_PRIVATE_KEY not set - Manual role grants required!"); + } else { + const adminSigner = new ethers.Wallet(adminPrivateKey, ethers.provider); + console.log("Using admin signer:", await adminSigner.getAddress()); + + const fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address, adminSigner); + const fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address, adminSigner); + const agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address, adminSigner); + + // 3.1 Grant CREATOR_ROLE of FFactoryV2 to BondingV5 + console.log("\n--- Granting CREATOR_ROLE of FFactoryV2 to BondingV5 ---"); + const creatorRole = await fFactoryV2.CREATOR_ROLE(); + await (await fFactoryV2.grantRole(creatorRole, bondingV5Address)).wait(); + console.log("✅ Granted CREATOR_ROLE of FFactoryV2 to BondingV5"); + + // 3.2 Set BondingV5 and BondingConfig in FRouterV2 + console.log("\n--- Setting BondingV5 in FRouterV2 ---"); + await (await fRouterV2.setBondingV5(bondingV5Address, bondingConfigAddress)).wait(); + console.log("✅ Set BondingV5 and BondingConfig in FRouterV2"); + + // 3.3 Grant EXECUTOR_ROLE of FRouterV2 to BondingV5 + console.log("\n--- Granting EXECUTOR_ROLE of FRouterV2 to BondingV5 ---"); + const executorRole = await fRouterV2.EXECUTOR_ROLE(); + await (await fRouterV2.grantRole(executorRole, bondingV5Address)).wait(); + console.log("✅ Granted EXECUTOR_ROLE of FRouterV2 to BondingV5"); + + // 3.4 Grant BONDING_ROLE of AgentFactoryV6 to BondingV5 + console.log("\n--- Granting BONDING_ROLE of AgentFactoryV6 to BondingV5 ---"); + const bondingRole = await agentFactoryV6.BONDING_ROLE(); + await (await agentFactoryV6.grantRole(bondingRole, bondingV5Address)).wait(); + console.log("✅ Granted BONDING_ROLE of AgentFactoryV6 to BondingV5"); + + console.log("\n✅ All role grants completed!"); + } + + // ============================================ + // 5. Print Deployment Summary + // ============================================ + console.log("\n=== LaunchpadV5 Deployment Summary ==="); + + console.log("\nPrerequisite contracts (reused):"); + console.log(`- FFactoryV2: ${fFactoryV2Address}`); + console.log(`- FRouterV2: ${fRouterV2Address}`); + console.log(`- AgentFactoryV6: ${agentFactoryV6Address}`); + + console.log("\nNewly deployed contracts:"); + console.log(`- BondingConfig: ${bondingConfigAddress}`); + console.log(`- BondingV5: ${bondingV5Address}`); + + console.log("\nConfiguration:"); + console.log("- Initial Supply:", initialSupply); + console.log("- Max Airdrop Percent:", maxAirdropPercent, "%"); + console.log("- Start Time Delay:", startTimeDelay, "seconds"); + console.log("- Normal Launch Fee:", normalLaunchFee, "VIRTUAL (scheduled/marketing)"); + console.log("- ACF Fee:", acfFee, "VIRTUAL (extra fee when needAcf = true)"); + console.log("- Team Token Reserved Wallet:", teamTokenReservedWallet); + console.log("- Fake Initial Virtual Liq:", fakeInitialVirtualLiq); + console.log("- Target Real Virtual:", targetRealVirtual); + + console.log("\nFee Structure:"); + console.log("- Immediate launch, no ACF: 0 VIRTUAL"); + console.log(`- Immediate launch, with ACF: ${acfFee} VIRTUAL`); + console.log(`- Scheduled launch, no ACF: ${normalLaunchFee} VIRTUAL`); + console.log(`- Scheduled launch, with ACF: ${normalLaunchFee} + ${acfFee} = ${Number(normalLaunchFee) + Number(acfFee)} VIRTUAL`); + + console.log("\nLaunch Modes:"); + console.log("- LAUNCH_MODE_NORMAL (0): Open to everyone"); + console.log("- LAUNCH_MODE_X_LAUNCH (1): Requires isXLauncher authorization"); + console.log("- LAUNCH_MODE_ACP_SKILL (2): Requires isAcpSkillLauncher authorization"); + + console.log("\n--- Deployment Order ---"); + console.log("1. ✅ deployLaunchpadv5_1.ts (FFactoryV2, FRouterV2) - DONE"); + console.log("2. ✅ deployLaunchpadv5_2.ts (AgentFactoryV6) - DONE"); + console.log("3. ✅ deployLaunchpadv5_3.ts (BondingConfig, BondingV5) - DONE"); + console.log("4. ⏳ deployLaunchpadv5_4.ts (Revoke deployer roles) - DONE"); + + console.log("\n" + "=".repeat(60)); + + console.log("\n--- Next Step ---"); + console.log("1. Run: npx hardhat run scripts/launchpadv5/deployLaunchpadv5_4.ts --network "); + + console.log("\nDeployment completed successfully!"); + } catch (e) { + console.error("Deployment failed:", e); + throw e; + } +})(); diff --git a/scripts/launchpadv5/deployLaunchpadv5_4.ts b/scripts/launchpadv5/deployLaunchpadv5_4.ts new file mode 100644 index 0000000..12e6113 --- /dev/null +++ b/scripts/launchpadv5/deployLaunchpadv5_4.ts @@ -0,0 +1,228 @@ +const { ethers } = require("hardhat"); + +// Environment variables are loaded via hardhat.config.js +// Make sure hardhat.config.js has: require("dotenv").config({ path: ".env.launchpadv5_dev" }); + +/** + * Revoke deployer roles from all contracts + * Run this script LAST after all deployments are complete + * + * Deployment order: + * 1. deployPrerequisites_1.ts - FFactoryV2, FRouterV2 + * 2. deployPrerequisites_2.ts - AgentFactoryV6 + * 3. deployLaunchpadv5.ts - BondingConfig, BondingV5 + * 4. deployRevokeRoles.ts - Revoke deployer roles (this script) + */ +(async () => { + try { + console.log("\n=== Revoking Deployer Roles ==="); + console.log( + "This script revokes all temporary deployer roles for security." + ); + + const [deployer] = await ethers.getSigners(); + const deployerAddress = await deployer.getAddress(); + console.log("Deployer address:", deployerAddress); + + // ============================================ + // Load contract addresses + // ============================================ + const fFactoryV2Address = process.env.FFactoryV2_ADDRESS; + if (!fFactoryV2Address) { + throw new Error("FFactoryV2_ADDRESS not set in environment"); + } + const fRouterV2Address = process.env.FRouterV2_ADDRESS; + if (!fRouterV2Address) { + throw new Error("FRouterV2_ADDRESS not set in environment"); + } + const agentFactoryV6Address = process.env.AGENT_FACTORY_V6_ADDRESS; + if (!agentFactoryV6Address) { + throw new Error("AGENT_FACTORY_V6_ADDRESS not set in environment"); + } + + // AgentNftV2 is optional (only if newly deployed in deployPrerequisites_2.ts) + const agentNftV2Address = process.env.AGENT_NFT_V2_ADDRESS; + + console.log("\nContract addresses:", { + fFactoryV2Address, + fRouterV2Address, + agentFactoryV6Address, + agentNftV2Address: agentNftV2Address || "(not set - skipping)", + }); + + // ============================================ + // Get contract instances + // ============================================ + const fFactoryV2 = await ethers.getContractAt( + "FFactoryV2", + fFactoryV2Address + ); + const fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address); + const agentFactoryV6 = await ethers.getContractAt( + "AgentFactoryV6", + agentFactoryV6Address + ); + + // ============================================ + // Revoke FFactoryV2 deployer roles + // ============================================ + console.log("\n--- Revoking FFactoryV2 deployer roles ---"); + + const fFactoryAdminRole = await fFactoryV2.ADMIN_ROLE(); + const fFactoryDefaultAdminRole = await fFactoryV2.DEFAULT_ADMIN_ROLE(); + + // Check if deployer has ADMIN_ROLE + if (await fFactoryV2.hasRole(fFactoryAdminRole, deployerAddress)) { + await ( + await fFactoryV2.revokeRole(fFactoryAdminRole, deployerAddress) + ).wait(); + console.log("✅ Revoked ADMIN_ROLE of FFactoryV2 from deployer"); + } else { + console.log( + "⏭️ Deployer doesn't have ADMIN_ROLE on FFactoryV2 (already revoked)" + ); + } + + // Check if deployer has DEFAULT_ADMIN_ROLE + if (await fFactoryV2.hasRole(fFactoryDefaultAdminRole, deployerAddress)) { + await ( + await fFactoryV2.revokeRole(fFactoryDefaultAdminRole, deployerAddress) + ).wait(); + console.log("✅ Revoked DEFAULT_ADMIN_ROLE of FFactoryV2 from deployer"); + } else { + console.log( + "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on FFactoryV2 (already revoked)" + ); + } + + // ============================================ + // Revoke FRouterV2 deployer roles + // ============================================ + console.log("\n--- Revoking FRouterV2 deployer roles ---"); + + const fRouterAdminRole = await fRouterV2.ADMIN_ROLE(); + const fRouterDefaultAdminRole = await fRouterV2.DEFAULT_ADMIN_ROLE(); + + // Check if deployer has ADMIN_ROLE + if (await fRouterV2.hasRole(fRouterAdminRole, deployerAddress)) { + await ( + await fRouterV2.revokeRole(fRouterAdminRole, deployerAddress) + ).wait(); + console.log("✅ Revoked ADMIN_ROLE of FRouterV2 from deployer"); + } else { + console.log( + "⏭️ Deployer doesn't have ADMIN_ROLE on FRouterV2 (already revoked)" + ); + } + + // Check if deployer has DEFAULT_ADMIN_ROLE + if (await fRouterV2.hasRole(fRouterDefaultAdminRole, deployerAddress)) { + await ( + await fRouterV2.revokeRole(fRouterDefaultAdminRole, deployerAddress) + ).wait(); + console.log("✅ Revoked DEFAULT_ADMIN_ROLE of FRouterV2 from deployer"); + } else { + console.log( + "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on FRouterV2 (already revoked)" + ); + } + + // ============================================ + // Revoke AgentFactoryV6 deployer roles + // ============================================ + console.log("\n--- Revoking AgentFactoryV6 deployer roles ---"); + + const agentFactoryDefaultAdminRole = + await agentFactoryV6.DEFAULT_ADMIN_ROLE(); + + // Check if deployer has DEFAULT_ADMIN_ROLE + if ( + await agentFactoryV6.hasRole( + agentFactoryDefaultAdminRole, + deployerAddress + ) + ) { + await ( + await agentFactoryV6.revokeRole( + agentFactoryDefaultAdminRole, + deployerAddress + ) + ).wait(); + console.log( + "✅ Revoked DEFAULT_ADMIN_ROLE of AgentFactoryV6 from deployer" + ); + } else { + console.log( + "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentFactoryV6 (already revoked)" + ); + } + + // ============================================ + // Revoke AgentNftV2 deployer roles (if applicable) + // ============================================ + if (agentNftV2Address) { + console.log("\n--- Revoking AgentNftV2 deployer roles ---"); + const agentNftV2 = await ethers.getContractAt( + "AgentNftV2", + agentNftV2Address + ); + const agentNftDefaultAdminRole = await agentNftV2.DEFAULT_ADMIN_ROLE(); + + // Check if deployer has DEFAULT_ADMIN_ROLE + if (await agentNftV2.hasRole(agentNftDefaultAdminRole, deployerAddress)) { + await ( + await agentNftV2.revokeRole(agentNftDefaultAdminRole, deployerAddress) + ).wait(); + console.log( + "✅ Revoked DEFAULT_ADMIN_ROLE of AgentNftV2 from deployer" + ); + } else { + console.log( + "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentNftV2 (already revoked)" + ); + } + } + + // ============================================ + // Summary + // ============================================ + console.log("\n=== Role Revocation Complete ==="); + console.log("All deployer roles have been revoked."); + console.log("\nDeployer should no longer have admin access to:"); + console.log(`- FFactoryV2: ${fFactoryV2Address}`); + console.log(`- FRouterV2: ${fRouterV2Address}`); + console.log(`- AgentFactoryV6: ${agentFactoryV6Address}`); + if (agentNftV2Address) { + console.log(`- AgentNftV2: ${agentNftV2Address}`); + } + + console.log("\n✅ Security hardening complete!"); + + console.log("\n--- Deployment Order ---"); + console.log("1. ✅ deployLaunchpadv5_1.ts (FFactoryV2, FRouterV2) - DONE"); + console.log("2. ✅ deployLaunchpadv5_2.ts (AgentFactoryV6) - DONE"); + console.log( + "3. ✅ deployLaunchpadv5_3.ts (BondingConfig, BondingV5) - DONE" + ); + console.log("4. ✅ deployLaunchpadv5_4.ts (Revoke deployer roles) - DONE"); + + console.log("\n" + "=".repeat(60)); + + console.log("\n=== NewLaunchpadV5 Deployment Summary ===\n"); + console.log("All contracts deployed and configured:"); + console.log(`- FFactoryV2: ${process.env.FFactoryV2_ADDRESS}`); + console.log(`- FRouterV2: ${process.env.FRouterV2_ADDRESS}`); + console.log(`- AgentFactoryV6: ${process.env.AGENT_FACTORY_V6_ADDRESS}`); + console.log(`- BondingConfig: ${process.env.BONDING_CONFIG_ADDRESS}`); + console.log(`- BondingV5: ${process.env.BONDING_V5_ADDRESS}`); + console.log(`- Virtual Token: ${process.env.VIRTUAL_TOKEN_ADDRESS}`); + console.log(`- AgentTokenV2: ${process.env.AGENT_TOKEN_V2_IMPLEMENTATION}`); + console.log(`- AgentVeTokenV2: ${process.env.AGENT_VE_TOKEN_V2_IMPLEMENTATION}`); + console.log(`- AgentDAOImpl: ${process.env.AGENT_DAO_IMPLEMENTATION}`); + console.log(`- AgentNftV2: ${process.env.AGENT_NFT_V2_ADDRESS}`); + console.log(`- AgentTaxManager: ${process.env.AGENT_TOKEN_TAX_MANAGER}`); + } catch (e) { + console.error("Role revocation failed:", e); + throw e; + } +})(); diff --git a/scripts/launchpadv5/e2e_test.ts b/scripts/launchpadv5/e2e_test.ts new file mode 100644 index 0000000..c058866 --- /dev/null +++ b/scripts/launchpadv5/e2e_test.ts @@ -0,0 +1,781 @@ +import { parseEther, formatEther } from "ethers"; +const { ethers } = require("hardhat"); + +// Constants for launch modes and anti-sniper types +const LAUNCH_MODE_NORMAL = 0; +const LAUNCH_MODE_X_LAUNCH = 1; +const LAUNCH_MODE_ACP_SKILL = 2; + +const ANTI_SNIPER_NONE = 0; +const ANTI_SNIPER_60S = 1; +const ANTI_SNIPER_98M = 2; + +/** + * Wait for a specified number of seconds with progress indicator + * Works on real networks (Base Sepolia, etc.) + */ +async function waitWithProgress(seconds: number, message: string): Promise { + console.log(`\n⏳ ${message}`); + console.log(` Waiting ${seconds} seconds...`); + + const startTime = Date.now(); + const endTime = startTime + (seconds * 1000); + + // Show progress every 10 seconds or 10% of total time, whichever is greater + const progressInterval = Math.max(10, Math.floor(seconds / 10)); + let lastProgress = 0; + + while (Date.now() < endTime) { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + const remaining = seconds - elapsed; + + if (elapsed - lastProgress >= progressInterval || remaining <= 5) { + console.log(` ⏱️ ${elapsed}s elapsed, ${remaining}s remaining...`); + lastProgress = elapsed; + } + + // Wait 1 second before next check + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + console.log(` ✅ Wait complete!`); +} + +interface TestConfig { + bondingV5Address: string; + bondingConfigAddress: string; + fFactoryV2Address: string; + fRouterV2Address: string; + virtualTokenAddress: string; +} + +async function main() { + console.log("\n" + "=".repeat(80)); + console.log(" BondingV5 E2E Test - Comprehensive Verification"); + console.log("=".repeat(80)); + + // Load contract addresses from environment + const config: TestConfig = { + bondingV5Address: process.env.BONDING_V5_ADDRESS || "", + bondingConfigAddress: process.env.BONDING_CONFIG_ADDRESS || "", + fFactoryV2Address: process.env.FFactoryV2_ADDRESS || "", + fRouterV2Address: process.env.FRouterV2_ADDRESS || "", + virtualTokenAddress: process.env.VIRTUAL_TOKEN_ADDRESS || "", + }; + + // Validate required addresses + for (const [key, value] of Object.entries(config)) { + if (!value) { + throw new Error(`${key} not set in environment`); + } + } + + console.log("\n--- Contract Addresses ---"); + console.log("BondingV5:", config.bondingV5Address); + console.log("BondingConfig:", config.bondingConfigAddress); + console.log("FFactoryV2:", config.fFactoryV2Address); + console.log("FRouterV2:", config.fRouterV2Address); + console.log("VIRTUAL Token:", config.virtualTokenAddress); + + // Get signer + const [signer] = await ethers.getSigners(); + const signerAddress = await signer.getAddress(); + console.log("\n--- Signer ---"); + console.log("Address:", signerAddress); + + // Get contract instances + const bondingV5 = await ethers.getContractAt("BondingV5", config.bondingV5Address); + const bondingConfig = await ethers.getContractAt("BondingConfig", config.bondingConfigAddress); + const fFactoryV2 = await ethers.getContractAt("FFactoryV2", config.fFactoryV2Address); + const fRouterV2 = await ethers.getContractAt("FRouterV2", config.fRouterV2Address); + const virtualToken = await ethers.getContractAt("IERC20", config.virtualTokenAddress); + + // ============================================ + // Step 1: Verify Configuration Parameters + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 1: Verify Configuration Parameters"); + console.log("=".repeat(80)); + + // Check BondingConfig parameters + const scheduledLaunchParams = await bondingConfig.scheduledLaunchParams(); + const bondingCurveParams = await bondingConfig.bondingCurveParams(); + const initialSupply = await bondingConfig.initialSupply(); + const maxAirdropPercent = await bondingConfig.maxAirdropPercent(); + const feeTo = await bondingConfig.feeTo(); + const teamTokenReservedWallet = await bondingConfig.teamTokenReservedWallet(); + + console.log("\n--- BondingConfig Parameters ---"); + console.log("Initial Supply:", initialSupply.toString()); + console.log("Max Airdrop Percent:", maxAirdropPercent.toString(), "%"); + console.log("Fee To:", feeTo); + console.log("Team Token Reserved Wallet:", teamTokenReservedWallet); + console.log("\n--- ScheduledLaunchParams ---"); + console.log("Start Time Delay:", scheduledLaunchParams.startTimeDelay.toString(), "seconds"); + console.log("Normal Launch Fee:", formatEther(scheduledLaunchParams.normalLaunchFee), "VIRTUAL"); + console.log("ACF Fee:", formatEther(scheduledLaunchParams.acfFee), "VIRTUAL"); + console.log("\n--- BondingCurveParams ---"); + console.log("Fake Initial Virtual Liq:", formatEther(bondingCurveParams.fakeInitialVirtualLiq), "VIRTUAL"); + console.log("Target Real Virtual:", formatEther(bondingCurveParams.targetRealVirtual), "VIRTUAL"); + + // Check FFactoryV2 tax parameters + const buyTax = await fFactoryV2.buyTax(); + const sellTax = await fFactoryV2.sellTax(); + const antiSniperBuyTaxStartValue = await fFactoryV2.antiSniperBuyTaxStartValue(); + const taxVault = await fFactoryV2.taxVault(); + const antiSniperTaxVault = await fFactoryV2.antiSniperTaxVault(); + + console.log("\n--- FFactoryV2 Tax Parameters ---"); + console.log("Buy Tax:", buyTax.toString(), "%"); + console.log("Sell Tax:", sellTax.toString(), "%"); + console.log("Anti-Sniper Buy Tax Start Value:", antiSniperBuyTaxStartValue.toString(), "%"); + console.log("Tax Vault:", taxVault); + console.log("Anti-Sniper Tax Vault:", antiSniperTaxVault); + + // ============================================ + // Step 2: Check Virtual Token Balance and Approve + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 2: Check VIRTUAL Token Balance and Approve"); + console.log("=".repeat(80)); + + const virtualBalance = await virtualToken.balanceOf(signerAddress); + console.log("VIRTUAL Balance:", formatEther(virtualBalance), "VIRTUAL"); + + // Approve if needed - always approve to ensure sufficient allowance + const currentAllowance = await virtualToken.allowance(signerAddress, config.bondingV5Address); + const requiredAllowance = parseEther("10000"); + console.log("\n--- Checking BondingV5 Allowance ---"); + console.log("Signer Address:", signerAddress); + console.log("BondingV5 Address:", config.bondingV5Address); + console.log("Current Allowance:", currentAllowance.toString(), `(${formatEther(currentAllowance)} VIRTUAL)`); + console.log("Required Allowance:", requiredAllowance.toString(), `(${formatEther(requiredAllowance)} VIRTUAL)`); + console.log("Allowance sufficient?", BigInt(currentAllowance) >= BigInt(requiredAllowance)); + + if (BigInt(currentAllowance) < BigInt(requiredAllowance)) { + console.log("\n--- Approving VIRTUAL tokens to BondingV5 ---"); + const approveTx = await virtualToken.approve(config.bondingV5Address, requiredAllowance); + await approveTx.wait(); + console.log("✅ Approved", formatEther(requiredAllowance), "VIRTUAL to BondingV5"); + + // Verify the approval + const newAllowance = await virtualToken.allowance(signerAddress, config.bondingV5Address); + console.log("New Allowance:", formatEther(newAllowance), "VIRTUAL"); + } else { + console.log("✅ Already approved sufficient VIRTUAL tokens"); + } + + // Also approve to FRouterV2 for buy/sell + const routerAllowance = await virtualToken.allowance(signerAddress, config.fRouterV2Address); + console.log("\n--- Checking FRouterV2 Allowance ---"); + console.log("FRouterV2 Address:", config.fRouterV2Address); + console.log("Current Router Allowance:", formatEther(routerAllowance), "VIRTUAL"); + + if (BigInt(routerAllowance) < BigInt(requiredAllowance)) { + console.log("\n--- Approving VIRTUAL tokens to FRouterV2 ---"); + const approveTx = await virtualToken.approve(config.fRouterV2Address, requiredAllowance); + await approveTx.wait(); + console.log("✅ Approved", formatEther(requiredAllowance), "VIRTUAL to FRouterV2"); + } else { + console.log("✅ Already approved sufficient VIRTUAL tokens to FRouterV2"); + } + + // ============================================ + // Step 3: Test preLaunch with Immediate Launch + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 3: Test preLaunch (Immediate Launch)"); + console.log("=".repeat(80)); + + // First, verify BondingV5 has required roles + console.log("\n--- Verifying BondingV5 Roles ---"); + const agentFactoryV6Address = process.env.AGENT_FACTORY_V6_ADDRESS; + if (agentFactoryV6Address) { + const agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address); + const bondingRole = await agentFactoryV6.BONDING_ROLE(); + const hasBondingRole = await agentFactoryV6.hasRole(bondingRole, config.bondingV5Address); + console.log("BondingV5 has BONDING_ROLE on AgentFactoryV6:", hasBondingRole); + if (!hasBondingRole) { + throw new Error("BondingV5 does not have BONDING_ROLE on AgentFactoryV6!"); + } + } + + const creatorRole = await fFactoryV2.CREATOR_ROLE(); + const hasCreatorRole = await fFactoryV2.hasRole(creatorRole, config.bondingV5Address); + console.log("BondingV5 has CREATOR_ROLE on FFactoryV2:", hasCreatorRole); + if (!hasCreatorRole) { + throw new Error("BondingV5 does not have CREATOR_ROLE on FFactoryV2!"); + } + + const executorRole = await fRouterV2.EXECUTOR_ROLE(); + const hasExecutorRole = await fRouterV2.hasRole(executorRole, config.bondingV5Address); + console.log("BondingV5 has EXECUTOR_ROLE on FRouterV2:", hasExecutorRole); + if (!hasExecutorRole) { + throw new Error("BondingV5 does not have EXECUTOR_ROLE on FRouterV2!"); + } + + const tokenName = `E2E Test Token ${Date.now()}`; + const tokenTicker = `E2E${Math.floor(Math.random() * 1000)}`; + const cores = [0, 1, 2, 4]; // Standard cores (0=text, 1=voice, 2=vision) + const description = "E2E Test Token for BondingV5"; + const image = "https://example.com/e2e-test.png"; + const urls = ["", "", "", ""]; + const purchaseAmount = parseEther("200"); // 1000 VIRTUAL + + // Immediate launch (startTime < now + scheduledLaunchStartTimeDelay) + const latestBlock = await ethers.provider.getBlock("latest"); + const currentTimestamp = Number(latestBlock!.timestamp); + const startTimeDelayNum = Number(scheduledLaunchParams.startTimeDelay); + const startTime = currentTimestamp + 100; // immediate launch (100 seconds from now) + + const launchMode = LAUNCH_MODE_NORMAL; + const airdropPercent = 3; // 3% airdrop + const needAcf = true; // Test with ACF fee + const antiSniperTaxType = ANTI_SNIPER_60S; // 60 seconds anti-sniper + const isProject60days = false; + + // Check if this is scheduled or immediate launch + const isScheduledLaunch = startTime >= currentTimestamp + startTimeDelayNum; + + console.log("\n--- preLaunch Parameters ---"); + console.log("Token Name:", tokenName); + console.log("Token Ticker:", tokenTicker); + console.log("Cores:", cores); + console.log("Purchase Amount:", formatEther(purchaseAmount), "VIRTUAL"); + console.log("Current Block Timestamp:", currentTimestamp); + console.log("Start Time:", startTime, `(${new Date(Number(startTime) * 1000).toISOString()})`); + console.log("Scheduled Launch Start Time Delay:", startTimeDelayNum, "seconds"); + console.log("Is Scheduled Launch:", isScheduledLaunch, "(expected: false - immediate launch)"); + console.log("Launch Mode:", launchMode, "(NORMAL)"); + console.log("Airdrop Percent:", airdropPercent, "%"); + console.log("Need ACF:", needAcf); + console.log("Anti-Sniper Tax Type:", antiSniperTaxType, "(60S)"); + console.log("Is Project 60 Days:", isProject60days); + + // Get feeTo balance before preLaunch + const feeToBalanceBefore = await virtualToken.balanceOf(feeTo); + + console.log("\n--- Executing preLaunch ---"); + + // Get fresh block timestamp right before the call to ensure accurate comparison + const freshBlock = await ethers.provider.getBlock("latest"); + const freshTimestamp = Number(freshBlock!.timestamp); + const scheduledThreshold = freshTimestamp + startTimeDelayNum; + + console.log("\n--- Time Diagnostics (Fresh) ---"); + console.log("Fresh Block Number:", freshBlock!.number); + console.log("Fresh Block Timestamp:", freshTimestamp, `(${new Date(freshTimestamp * 1000).toISOString()})`); + console.log("Scheduled Threshold:", scheduledThreshold, `(now + ${startTimeDelayNum}s)`); + console.log("Our startTime:", startTime); + console.log("startTime < scheduledThreshold?", startTime < scheduledThreshold, "(should be true for immediate launch)"); + console.log("Difference:", startTime - freshTimestamp, "seconds from now"); + + // First, try staticCall to get the revert reason if any + try { + console.log("\n--- Running staticCall to check for errors ---"); + await bondingV5.preLaunch.staticCall( + tokenName, + tokenTicker, + cores, + description, + image, + urls, + purchaseAmount, + startTime, + launchMode, + airdropPercent, + needAcf, + antiSniperTaxType, + isProject60days + ); + console.log("✅ staticCall passed, proceeding with actual transaction..."); + } catch (staticCallError: any) { + console.error("\n❌ staticCall failed - this will likely fail on-chain too:"); + console.error("Error:", staticCallError.message); + if (staticCallError.reason) { + console.error("Reason:", staticCallError.reason); + } + if (staticCallError.revert) { + console.error("Revert:", staticCallError.revert); + } + if (staticCallError.data) { + console.error("Data:", staticCallError.data); + } + // Try to decode custom error + try { + const errorData = staticCallError.data; + if (errorData && errorData !== "0x") { + console.error("Error Selector:", errorData.slice(0, 10)); + } + } catch {} + throw staticCallError; + } + + // Estimate gas first to see what's needed + const estimatedGas = await bondingV5.preLaunch.estimateGas( + tokenName, + tokenTicker, + cores, + description, + image, + urls, + purchaseAmount, + startTime, + launchMode, + airdropPercent, + needAcf, + antiSniperTaxType, + isProject60days + ); + console.log("Estimated Gas:", estimatedGas.toString()); + + // Use 150% of estimated gas as limit + const gasLimit = (estimatedGas * 150n) / 100n; + console.log("Using Gas Limit:", gasLimit.toString()); + + const preLaunchTx = await bondingV5.preLaunch( + tokenName, + tokenTicker, + cores, + description, + image, + urls, + purchaseAmount, + startTime, + launchMode, + airdropPercent, + needAcf, + antiSniperTaxType, + isProject60days, + { gasLimit } + ); + + const preLaunchReceipt = await preLaunchTx.wait(); + console.log("✅ preLaunch transaction successful!"); + console.log("Gas Used:", preLaunchReceipt.gasUsed.toString()); + + // Parse PreLaunched event + const preLaunchedEvent = preLaunchReceipt.logs.find((log: any) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed?.name === "PreLaunched"; + } catch { + return false; + } + }); + + if (!preLaunchedEvent) { + throw new Error("PreLaunched event not found"); + } + + const parsedEvent = bondingV5.interface.parseLog(preLaunchedEvent); + const tokenAddress = parsedEvent.args.token; + const pairAddress = parsedEvent.args.pair; + const virtualId = parsedEvent.args.virtualId; + const initialPurchase = parsedEvent.args.initialPurchase; + const eventLaunchParams = parsedEvent.args.launchParams; + + console.log("\n--- PreLaunched Event Data ---"); + console.log("Token Address:", tokenAddress); + console.log("Pair Address:", pairAddress); + console.log("Virtual ID:", virtualId.toString()); + console.log("Initial Purchase:", formatEther(initialPurchase), "VIRTUAL"); + console.log("LaunchParams from Event:", { + launchMode: eventLaunchParams.launchMode, + airdropPercent: eventLaunchParams.airdropPercent, + needAcf: eventLaunchParams.needAcf, + antiSniperTaxType: eventLaunchParams.antiSniperTaxType, + isProject60days: eventLaunchParams.isProject60days, + }); + + // ============================================ + // Step 4: Verify On-Chain Parameters + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 4: Verify On-Chain Parameters"); + console.log("=".repeat(80)); + + // Verify tokenInfo + const tokenInfo = await bondingV5.tokenInfo(tokenAddress); + console.log("\n--- tokenInfo ---"); + console.log("Creator:", tokenInfo.creator); + console.log("Token:", tokenInfo.token); + console.log("Pair:", tokenInfo.pair); + console.log("Description:", tokenInfo.description); + console.log("Trading:", tokenInfo.trading); + console.log("Trading on Uniswap:", tokenInfo.tradingOnUniswap); + console.log("Launch Executed:", tokenInfo.launchExecuted); + console.log("Initial Purchase:", formatEther(tokenInfo.initialPurchase), "VIRTUAL"); + + // Verify tokenLaunchParams + const onChainLaunchParams = await bondingV5.tokenLaunchParams(tokenAddress); + console.log("\n--- tokenLaunchParams (On-Chain) ---"); + console.log("Launch Mode:", onChainLaunchParams.launchMode, "(expected:", launchMode, ")"); + console.log("Airdrop Percent:", onChainLaunchParams.airdropPercent, "(expected:", airdropPercent, ")"); + console.log("Need ACF:", onChainLaunchParams.needAcf, "(expected:", needAcf, ")"); + console.log("Anti-Sniper Tax Type:", onChainLaunchParams.antiSniperTaxType, "(expected:", antiSniperTaxType, ")"); + console.log("Is Project 60 Days:", onChainLaunchParams.isProject60days, "(expected:", isProject60days, ")"); + + // Verify view functions + const isProject60daysResult = await bondingV5.isProject60days(tokenAddress); + const tokenAntiSniperTypeResult = await bondingV5.tokenAntiSniperType(tokenAddress); + const tokenGradThreshold = await bondingV5.tokenGradThreshold(tokenAddress); + + console.log("\n--- View Functions ---"); + console.log("isProject60days():", isProject60daysResult, "(expected:", isProject60days, ")"); + console.log("tokenAntiSniperType():", tokenAntiSniperTypeResult, "(expected:", antiSniperTaxType, ")"); + console.log("tokenGradThreshold():", formatEther(tokenGradThreshold), "tokens"); + + // Verify anti-sniper duration from BondingConfig + const antiSniperDuration = await bondingConfig.getAntiSniperDuration(antiSniperTaxType); + console.log("\n--- Anti-Sniper Tax Duration ---"); + console.log("Duration:", antiSniperDuration.toString(), "seconds (expected: 60)"); + + // ============================================ + // Step 5: Verify Fee Calculation + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 5: Verify Fee Calculation"); + console.log("=".repeat(80)); + + // Check feeTo balance after preLaunch + const feeToBalanceAfter = await virtualToken.balanceOf(feeTo); + const feeCollected = feeToBalanceAfter - feeToBalanceBefore; + + // Calculate expected fee using BondingConfig.calculateLaunchFee logic + // For scheduled launch (startTime >= currentTimestamp + startTimeDelay): + // - needAcf=false: normalLaunchFee (100 VIRTUAL) + // - needAcf=true: normalLaunchFee + acfFee (110 VIRTUAL) + // For immediate launch (startTime < currentTimestamp + startTimeDelay): + // - needAcf=false: 0 (free) + // - needAcf=true: acfFee only (10 VIRTUAL) + let expectedFee: bigint; + if (isScheduledLaunch) { + expectedFee = needAcf + ? scheduledLaunchParams.normalLaunchFee + scheduledLaunchParams.acfFee + : scheduledLaunchParams.normalLaunchFee; + } else { + expectedFee = needAcf ? scheduledLaunchParams.acfFee : 0n; + } + + console.log("Fee To Balance Before:", formatEther(feeToBalanceBefore), "VIRTUAL"); + console.log("Fee To Balance After:", formatEther(feeToBalanceAfter), "VIRTUAL"); + console.log("Fee Collected:", formatEther(feeCollected), "VIRTUAL"); + console.log("Expected Fee:", formatEther(expectedFee), "VIRTUAL", `(${isScheduledLaunch ? "scheduled" : "immediate"}, needAcf=${needAcf})`); + + if (BigInt(feeCollected) === BigInt(expectedFee)) { + console.log("✅ Fee calculation correct!"); + } else { + console.log("⚠️ Fee mismatch - expected:", formatEther(expectedFee), "got:", formatEther(feeCollected)); + } + + // ============================================ + // Step 6: Wait for Start Time and Launch + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 6: Wait for Start Time and Launch"); + console.log("=".repeat(80)); + + // Get pair start time + const pair = await ethers.getContractAt("IFPairV2", pairAddress); + const pairStartTime = await pair.startTime(); + console.log("Pair Start Time:", new Date(Number(pairStartTime) * 1000).toISOString()); + + // Calculate wait time + const currentTime = Math.floor(Date.now() / 1000); + const waitTime = Number(pairStartTime) - currentTime; + + if (waitTime > 0) { + await waitWithProgress(waitTime + 2, "Waiting for pair start time to be reached..."); + } else { + console.log("✅ Start time already passed, can proceed with launch"); + } + + console.log("\n--- Executing launch ---"); + const launchTx = await bondingV5.launch(tokenAddress, { gasLimit: 3000000 }); + const launchReceipt = await launchTx.wait(); + console.log("✅ launch() transaction successful!"); + console.log("Gas Used:", launchReceipt.gasUsed.toString()); + + // Parse Launched event + const launchedEvent = launchReceipt.logs.find((log: any) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed?.name === "Launched"; + } catch { + return false; + } + }); + + if (launchedEvent) { + const parsedLaunchedEvent = bondingV5.interface.parseLog(launchedEvent); + console.log("\n--- Launched Event Data ---"); + console.log("Initial Purchase Amount:", formatEther(parsedLaunchedEvent.args.initialPurchase), "VIRTUAL"); + console.log("Initial Purchased Amount:", formatEther(parsedLaunchedEvent.args.initialPurchasedAmount), "tokens"); + } + + // Verify token status after launch + const tokenInfoAfterLaunch = await bondingV5.tokenInfo(tokenAddress); + console.log("\n--- Token Status After Launch ---"); + console.log("Launch Executed:", tokenInfoAfterLaunch.launchExecuted); + console.log("Trading:", tokenInfoAfterLaunch.trading); + + // ============================================ + // Step 7: Test Buy with Anti-Sniper Tax Verification + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 7: Test Buy with Anti-Sniper Tax Verification"); + console.log("=".repeat(80)); + + // Get agent token contract + const agentToken = await ethers.getContractAt("IERC20", tokenAddress); + + // Check if we're in the anti-sniper period + const hasAntiSniperTax = await fRouterV2.hasAntiSniperTax(pairAddress); + const taxStartTime = await pair.taxStartTime(); + const currentBlockTime = (await ethers.provider.getBlock("latest")).timestamp; + const timeSinceLaunch = currentBlockTime - Number(taxStartTime); + + console.log("\n--- Anti-Sniper Tax Status ---"); + console.log("Tax Start Time:", new Date(Number(taxStartTime) * 1000).toISOString()); + console.log("Current Block Time:", new Date(currentBlockTime * 1000).toISOString()); + console.log("Time Since Launch:", timeSinceLaunch, "seconds"); + console.log("Anti-Sniper Duration:", antiSniperDuration.toString(), "seconds"); + console.log("Has Anti-Sniper Tax Active:", hasAntiSniperTax); + + // Get tax vault balances before buy + const taxVaultBalanceBefore = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceBefore = await virtualToken.balanceOf(antiSniperTaxVault); + + console.log("\n--- Tax Vault Balances Before Buy ---"); + console.log("Tax Vault Balance:", formatEther(taxVaultBalanceBefore), "VIRTUAL"); + console.log("Anti-Sniper Tax Vault Balance:", formatEther(antiSniperTaxVaultBalanceBefore), "VIRTUAL"); + + const buyAmount = parseEther("100"); // 100 VIRTUAL + const deadline = Math.floor(Date.now() / 1000) + 300; // 5 minutes + + const agentTokenBalanceBefore = await agentToken.balanceOf(signerAddress); + + console.log("\n--- Buy Parameters ---"); + console.log("Buy Amount:", formatEther(buyAmount), "VIRTUAL"); + console.log("Agent Token Balance Before:", formatEther(agentTokenBalanceBefore), "tokens"); + + console.log("\n--- Executing buy (during anti-sniper period) ---"); + const buyTx = await bondingV5.buy(buyAmount, tokenAddress, 0, deadline, { gasLimit: 500000 }); + const buyReceipt = await buyTx.wait(); + console.log("✅ buy() transaction successful!"); + console.log("Gas Used:", buyReceipt.gasUsed.toString()); + + const agentTokenBalanceAfterBuy = await agentToken.balanceOf(signerAddress); + const tokensReceived = agentTokenBalanceAfterBuy - agentTokenBalanceBefore; + console.log("Agent Token Balance After:", formatEther(agentTokenBalanceAfterBuy), "tokens"); + console.log("Tokens Received:", formatEther(tokensReceived), "tokens"); + + // Get tax vault balances after buy + const taxVaultBalanceAfterBuy = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceAfterBuy = await virtualToken.balanceOf(antiSniperTaxVault); + + const normalTaxCollected = taxVaultBalanceAfterBuy - taxVaultBalanceBefore; + const antiSniperTaxCollected = antiSniperTaxVaultBalanceAfterBuy - antiSniperTaxVaultBalanceBefore; + + console.log("\n--- Tax Vault Balances After Buy ---"); + console.log("Tax Vault Balance:", formatEther(taxVaultBalanceAfterBuy), "VIRTUAL"); + console.log("Anti-Sniper Tax Vault Balance:", formatEther(antiSniperTaxVaultBalanceAfterBuy), "VIRTUAL"); + console.log("\n--- Tax Collected ---"); + console.log("Normal Tax Collected:", formatEther(normalTaxCollected), "VIRTUAL"); + console.log("Anti-Sniper Tax Collected:", formatEther(antiSniperTaxCollected), "VIRTUAL"); + + // Verify anti-sniper tax was collected (if within anti-sniper period) + if (hasAntiSniperTax) { + // Calculate expected anti-sniper tax + // Anti-sniper tax decreases linearly from antiSniperBuyTaxStartValue (e.g., 99%) to 0% over the duration + const elapsedTime = BigInt(currentBlockTime) - taxStartTime; + const remainingTime = antiSniperDuration - elapsedTime; + const expectedAntiSniperTaxRate = remainingTime > 0n + ? (BigInt(antiSniperBuyTaxStartValue) * remainingTime) / antiSniperDuration + : 0n; + + console.log("\n--- Anti-Sniper Tax Verification ---"); + console.log("elapsedTime= ", elapsedTime.toString(), ", remainingTime= ", remainingTime.toString()); + console.log("Expected Anti-Sniper Tax Rate:", expectedAntiSniperTaxRate.toString(), "%"); + console.log("Normal Buy Tax Rate:", buyTax.toString(), "%"); + + if (BigInt(antiSniperTaxCollected) > 0n) { + console.log("✅ Anti-Sniper Tax correctly collected to antiSniperTaxVault"); + } else if (BigInt(normalTaxCollected) > 0n) { + console.log("⚠️ Only normal tax collected (anti-sniper period may have ended)"); + } + } else { + console.log("\n--- Normal Tax Verification ---"); + console.log("No anti-sniper tax active, only normal tax should be collected"); + if (BigInt(normalTaxCollected) > 0n) { + console.log("✅ Normal Tax correctly collected to taxVault"); + } + } + + // ============================================ + // Step 8: Test Buy After Anti-Sniper Period (if applicable) + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 8: Wait for Anti-Sniper Period to End and Test Buy"); + console.log("=".repeat(80)); + + // Wait for anti-sniper period to end if still active + const hasAntiSniperTaxNow = await fRouterV2.hasAntiSniperTax(pairAddress); + if (hasAntiSniperTaxNow) { + const currentTime2 = (await ethers.provider.getBlock("latest")).timestamp; + const remainingAntiSniperTime = Number(taxStartTime) + Number(antiSniperDuration) - currentTime2; + + if (remainingAntiSniperTime > 0) { + await waitWithProgress( + remainingAntiSniperTime + 5, + `Waiting for anti-sniper period to end (${antiSniperDuration} seconds total)...` + ); + } else { + console.log("✅ Anti-sniper period already ended"); + } + } else { + console.log("✅ No anti-sniper tax was active"); + } + + // Verify anti-sniper tax is no longer active + const hasAntiSniperTaxAfterWait = await fRouterV2.hasAntiSniperTax(pairAddress); + console.log("\nAnti-Sniper Tax Active After Wait:", hasAntiSniperTaxAfterWait); + + // Get tax vault balances before second buy + const taxVaultBalanceBeforeBuy2 = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceBeforeBuy2 = await virtualToken.balanceOf(antiSniperTaxVault); + + console.log("\n--- Tax Vault Balances Before Second Buy ---"); + console.log("Tax Vault Balance:", formatEther(taxVaultBalanceBeforeBuy2), "VIRTUAL"); + console.log("Anti-Sniper Tax Vault Balance:", formatEther(antiSniperTaxVaultBalanceBeforeBuy2), "VIRTUAL"); + + const buyAmount2 = parseEther("50"); // 50 VIRTUAL + const deadline2 = Math.floor(Date.now() / 1000) + 300; + + console.log("\n--- Executing buy (after anti-sniper period) ---"); + const buyTx2 = await bondingV5.buy(buyAmount2, tokenAddress, 0, deadline2, { gasLimit: 500000 }); + const buyReceipt2 = await buyTx2.wait(); + console.log("✅ buy() transaction successful!"); + console.log("Gas Used:", buyReceipt2.gasUsed.toString()); + + // Get tax vault balances after second buy + const taxVaultBalanceAfterBuy2 = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceAfterBuy2 = await virtualToken.balanceOf(antiSniperTaxVault); + + const normalTaxCollected2 = taxVaultBalanceAfterBuy2 - taxVaultBalanceBeforeBuy2; + const antiSniperTaxCollected2 = antiSniperTaxVaultBalanceAfterBuy2 - antiSniperTaxVaultBalanceBeforeBuy2; + + console.log("\n--- Tax Collected After Anti-Sniper Period ---"); + console.log("Normal Tax Collected:", formatEther(normalTaxCollected2), "VIRTUAL"); + console.log("Anti-Sniper Tax Collected:", formatEther(antiSniperTaxCollected2), "VIRTUAL"); + + // Calculate expected normal tax + const expectedNormalTax2 = (buyAmount2 * BigInt(buyTax)) / 100n; + console.log("Expected Normal Tax (", buyTax.toString(), "% of", formatEther(buyAmount2), "):", formatEther(expectedNormalTax2), "VIRTUAL"); + + if (!hasAntiSniperTaxAfterWait && BigInt(normalTaxCollected2) > 0n && BigInt(antiSniperTaxCollected2) === 0n) { + console.log("✅ After anti-sniper period: Only normal tax collected (no anti-sniper tax)"); + } else if (hasAntiSniperTaxAfterWait) { + console.log("⚠️ Anti-sniper period still active (may need longer wait time)"); + } + + // ============================================ + // Step 9: Test Sell with Tax Verification + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 9: Test Sell with Tax Verification"); + console.log("=".repeat(80)); + + // Get updated token balance + const agentTokenBalanceForSell = await agentToken.balanceOf(signerAddress); + + // Approve agent token to FRouterV2 + const sellAmount = BigInt(agentTokenBalanceForSell) / 4n; // Sell 1/4 of holdings + console.log("Sell Amount:", formatEther(sellAmount), "tokens"); + + const agentTokenAllowance = await agentToken.allowance(signerAddress, config.fRouterV2Address); + if (agentTokenAllowance < sellAmount) { + console.log("\n--- Approving Agent Token to FRouterV2 ---"); + const approveAgentTx = await agentToken.approve(config.fRouterV2Address, ethers.MaxUint256); + await approveAgentTx.wait(); + console.log("✅ Approved Agent Token to FRouterV2"); + } + + // Get balances before sell + const virtualBalanceBeforeSell = await virtualToken.balanceOf(signerAddress); + const taxVaultBalanceBeforeSell = await virtualToken.balanceOf(taxVault); + + console.log("\n--- Balances Before Sell ---"); + console.log("VIRTUAL Balance:", formatEther(virtualBalanceBeforeSell), "VIRTUAL"); + console.log("Tax Vault Balance:", formatEther(taxVaultBalanceBeforeSell), "VIRTUAL"); + + console.log("\n--- Executing sell ---"); + const sellDeadline = Math.floor(Date.now() / 1000) + 300; + const sellTx = await bondingV5.sell(sellAmount, tokenAddress, 0, sellDeadline, { gasLimit: 500000 }); + const sellReceipt = await sellTx.wait(); + console.log("✅ sell() transaction successful!"); + console.log("Gas Used:", sellReceipt.gasUsed.toString()); + + // Get balances after sell + const virtualBalanceAfterSell = await virtualToken.balanceOf(signerAddress); + const taxVaultBalanceAfterSell = await virtualToken.balanceOf(taxVault); + + const virtualReceived = virtualBalanceAfterSell - virtualBalanceBeforeSell; + const sellTaxCollected = taxVaultBalanceAfterSell - taxVaultBalanceBeforeSell; + + console.log("\n--- Balances After Sell ---"); + console.log("VIRTUAL Balance:", formatEther(virtualBalanceAfterSell), "VIRTUAL"); + console.log("Tax Vault Balance:", formatEther(taxVaultBalanceAfterSell), "VIRTUAL"); + console.log("\n--- Sell Results ---"); + console.log("VIRTUAL Received:", formatEther(virtualReceived), "VIRTUAL"); + console.log("Sell Tax Collected:", formatEther(sellTaxCollected), "VIRTUAL"); + console.log("Sell Tax Rate:", sellTax.toString(), "%"); + + if (BigInt(sellTaxCollected) > 0n) { + console.log("✅ Sell tax correctly collected to taxVault"); + } + + // ============================================ + // Summary + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" E2E Test Summary"); + console.log("=".repeat(80)); + + console.log("\n✅ All tests completed!"); + console.log("\nVerified:"); + console.log(" 1. BondingConfig parameters are correctly configured"); + console.log(" 2. FFactoryV2 tax parameters are correctly configured"); + console.log(" 3. preLaunch() executed successfully with new parameters"); + console.log(" 4. On-chain tokenInfo stored correctly"); + console.log(" 5. On-chain tokenLaunchParams stored correctly"); + console.log(" 6. Anti-sniper tax type correctly set:", antiSniperTaxType); + console.log(" 7. Anti-sniper duration:", antiSniperDuration.toString(), "seconds"); + console.log(" 8. Fee calculation correct (ACF fee for immediate launch)"); + console.log(" 9. launch() executed successfully after start time"); + console.log(" 10. buy() during anti-sniper period - tax to antiSniperTaxVault"); + console.log(" 11. buy() after anti-sniper period - normal tax to taxVault"); + console.log(" 12. sell() executed with correct tax collection"); + + console.log("\n--- Tax Summary ---"); + console.log("Normal Buy Tax Rate:", buyTax.toString(), "%"); + console.log("Normal Sell Tax Rate:", sellTax.toString(), "%"); + console.log("Anti-Sniper Buy Tax Start Value:", antiSniperBuyTaxStartValue.toString(), "%"); + console.log("Anti-Sniper Duration:", antiSniperDuration.toString(), "seconds"); + console.log("Tax Vault:", taxVault); + console.log("Anti-Sniper Tax Vault:", antiSniperTaxVault); + + console.log("\n--- Token Summary ---"); + console.log("Token Address:", tokenAddress); + console.log("Pair Address:", pairAddress); + console.log("Virtual ID:", virtualId.toString()); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error("\n❌ E2E Test Failed:"); + console.error(error); + process.exit(1); + }); diff --git a/test/launchpadv5/bondingV5.js b/test/launchpadv5/bondingV5.js new file mode 100644 index 0000000..29ea287 --- /dev/null +++ b/test/launchpadv5/bondingV5.js @@ -0,0 +1,2274 @@ +const { expect } = require("chai"); +const { ethers, upgrades, network } = require("hardhat"); +const { time } = require("@nomicfoundation/hardhat-network-helpers"); +const { + loadFixture, +} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); + +const { + expectTokenBalanceEqual, + expectApproximatelyEqual, + increaseTimeByMinutes, + increaseTimeAndMine, + increaseTimeByDays, +} = require("../launchpadv2/util.js"); + +const { + ERR_INVALID_TOKEN_STATUS, + ERR_INVALID_INPUT, + ERR_SLIPPAGE_TOO_HIGH, + ERR_ZERO_ADDRESSES, + ERR_AMOUNT_MUST_BE_GREATER_THAN_ZERO, + START_TIME_DELAY, + INITIAL_SUPPLY, + ERR_INVALID_START_TIME, + TBA_SALT, + TBA_IMPLEMENTATION, + DAO_VOTING_PERIOD, + DAO_THRESHOLD, + BUY_TAX, + SELL_TAX, + ANTI_SNIPER_BUY_TAX_START_VALUE, + FFactoryV2_TAX_VAULT, + FFactoryV2_ANTI_SNIPER_TAX_VAULT, +} = require("../launchpadv2/const.js"); + +// BondingV5 launch mode constants (only 3 modes now) +const LAUNCH_MODE_NORMAL = 0; +const LAUNCH_MODE_X_LAUNCH = 1; +const LAUNCH_MODE_ACP_SKILL = 2; + +// Max airdrop percent +const MAX_AIRDROP_PERCENT = 5; + +// Anti-sniper tax type constants +const ANTI_SNIPER_NONE = 0; +const ANTI_SNIPER_60S = 1; +const ANTI_SNIPER_98M = 2; + +// Fee structure +const NORMAL_LAUNCH_FEE = ethers.parseEther("100"); // Fee for scheduled/marketing launches +const ACF_FEE = ethers.parseEther("10"); // Extra fee when needAcf = true (10 on base, 150 on eth) + +// Bonding curve params +const FAKE_INITIAL_VIRTUAL_LIQ = ethers.parseEther("6300"); +const TARGET_REAL_VIRTUAL = ethers.parseEther("42000"); + +/** + * Setup function for BondingV5 tests + * Deploys all necessary contracts following the same order as deployment scripts: + * - Step 1: FFactoryV2 and FRouterV2 (like deployLaunchpadv5_1.ts) + * - Step 2: AgentFactoryV6 and dependencies (like deployLaunchpadv5_2.ts) + * - Step 3: BondingConfig and BondingV5, then grant roles (like deployLaunchpadv5_3.ts) + */ +async function setupBondingV5Test() { + const setup = {}; + + console.log("\n=== BondingV5 Test Setup Starting ==="); + const [owner, admin, beOpsWallet, user1, user2] = await ethers.getSigners(); + console.log("Owner address:", await owner.getAddress()); + console.log("Admin address:", await admin.getAddress()); + console.log("BE Ops Wallet address:", await beOpsWallet.getAddress()); + + try { + // ============================================ + // Step 0: Deploy Virtual Token (test prerequisite) + // ============================================ + console.log("\n--- Deploying MockERC20 for Virtual Token ---"); + const VirtualToken = await ethers.getContractFactory("MockERC20"); + const virtualToken = await VirtualToken.deploy( + "Virtual Token", + "VT", + owner.address, + ethers.parseEther("10000000000") + ); + await virtualToken.waitForDeployment(); + console.log("MockERC20 Virtual Token deployed at:", await virtualToken.getAddress()); + + // ============================================ + // Step 1: Deploy FFactoryV2 and FRouterV2 (like deployLaunchpadv5_1.ts) + // ============================================ + console.log("\n=== Step 1: Deploying FFactoryV2 and FRouterV2 ==="); + + // 1.1 Deploy FFactoryV2 + console.log("\n--- Deploying FFactoryV2 ---"); + const FFactoryV2 = await ethers.getContractFactory("FFactoryV2"); + const fFactoryV2 = await upgrades.deployProxy( + FFactoryV2, + [FFactoryV2_TAX_VAULT, BUY_TAX, SELL_TAX, ANTI_SNIPER_BUY_TAX_START_VALUE, FFactoryV2_ANTI_SNIPER_TAX_VAULT], + { initializer: "initialize" } + ); + await fFactoryV2.waitForDeployment(); + console.log("FFactoryV2 deployed at:", await fFactoryV2.getAddress()); + + // 1.2 Deploy FRouterV2 + console.log("\n--- Deploying FRouterV2 ---"); + const FRouterV2 = await ethers.getContractFactory("FRouterV2"); + const fRouterV2 = await upgrades.deployProxy( + FRouterV2, + [await fFactoryV2.getAddress(), await virtualToken.getAddress()], + { initializer: "initialize" } + ); + await fRouterV2.waitForDeployment(); + console.log("FRouterV2 deployed at:", await fRouterV2.getAddress()); + + // 1.3 Configure FFactoryV2 + console.log("\n--- Configuring FFactoryV2 ---"); + await fFactoryV2.grantRole(await fFactoryV2.ADMIN_ROLE(), owner.address); + console.log("ADMIN_ROLE granted to owner (deployer) in FFactoryV2"); + + await fFactoryV2.setRouter(await fRouterV2.getAddress()); + console.log("Router set in FFactoryV2"); + + await fFactoryV2.grantRole(await fFactoryV2.ADMIN_ROLE(), admin.address); + console.log("ADMIN_ROLE granted to admin in FFactoryV2"); + + await fFactoryV2.grantRole(await fFactoryV2.DEFAULT_ADMIN_ROLE(), admin.address); + console.log("DEFAULT_ADMIN_ROLE granted to admin in FFactoryV2"); + + // 1.4 Configure FRouterV2 + console.log("\n--- Configuring FRouterV2 ---"); + await fRouterV2.grantRole(await fRouterV2.ADMIN_ROLE(), admin.address); + console.log("ADMIN_ROLE granted to admin in FRouterV2"); + + await fRouterV2.grantRole(await fRouterV2.DEFAULT_ADMIN_ROLE(), admin.address); + console.log("DEFAULT_ADMIN_ROLE granted to admin in FRouterV2"); + + await fRouterV2.grantRole(await fRouterV2.EXECUTOR_ROLE(), beOpsWallet.address); + console.log("EXECUTOR_ROLE granted to BE_OPS_WALLET in FRouterV2"); + + // ============================================ + // Step 2: Deploy AgentFactoryV6 and dependencies (like deployLaunchpadv5_2.ts) + // ============================================ + console.log("\n=== Step 2: Deploying AgentFactoryV6 and dependencies ==="); + + // 2.1 Deploy AgentTokenV2 implementation + console.log("\n--- Deploying AgentTokenV2 implementation ---"); + const AgentTokenV2 = await ethers.getContractFactory("AgentTokenV2"); + const agentTokenV2 = await AgentTokenV2.deploy(); + await agentTokenV2.waitForDeployment(); + console.log("AgentTokenV2 implementation deployed at:", await agentTokenV2.getAddress()); + + // 2.2 Deploy AgentVeTokenV2 implementation + console.log("\n--- Deploying AgentVeTokenV2 implementation ---"); + const AgentVeTokenV2 = await ethers.getContractFactory("AgentVeTokenV2"); + const agentVeTokenV2 = await AgentVeTokenV2.deploy(); + await agentVeTokenV2.waitForDeployment(); + console.log("AgentVeTokenV2 implementation deployed at:", await agentVeTokenV2.getAddress()); + + // 2.3 Deploy MockAgentDAO implementation + console.log("\n--- Deploying MockAgentDAO implementation ---"); + const MockAgentDAO = await ethers.getContractFactory("MockAgentDAO"); + const mockAgentDAO = await MockAgentDAO.deploy(); + await mockAgentDAO.waitForDeployment(); + console.log("MockAgentDAO implementation deployed at:", await mockAgentDAO.getAddress()); + + // 2.4 Deploy MockERC6551Registry + console.log("\n--- Deploying MockERC6551Registry ---"); + const MockERC6551Registry = await ethers.getContractFactory("MockERC6551Registry"); + const mockERC6551Registry = await MockERC6551Registry.deploy(); + await mockERC6551Registry.waitForDeployment(); + console.log("MockERC6551Registry deployed at:", await mockERC6551Registry.getAddress()); + + // 2.5 Deploy AgentNftV2 + console.log("\n--- Deploying AgentNftV2 ---"); + const AgentNftV2 = await ethers.getContractFactory("AgentNftV2"); + const agentNftV2 = await upgrades.deployProxy(AgentNftV2, [owner.address], { + initializer: "initialize", + unsafeAllow: ["internal-function-storage"], + }); + await agentNftV2.waitForDeployment(); + console.log("AgentNftV2 deployed at:", await agentNftV2.getAddress()); + + // 2.6 Deploy MockUniswapV2Factory and Router + console.log("\n--- Deploying MockUniswapV2Factory ---"); + const MockUniswapV2Factory = await ethers.getContractFactory("MockUniswapV2Factory"); + const mockUniswapFactory = await MockUniswapV2Factory.deploy(); + await mockUniswapFactory.waitForDeployment(); + console.log("MockUniswapV2Factory deployed at:", await mockUniswapFactory.getAddress()); + + console.log("\n--- Deploying MockUniswapV2Router02 ---"); + const MockUniswapV2Router02 = await ethers.getContractFactory("MockUniswapV2Router02"); + const mockUniswapRouter = await MockUniswapV2Router02.deploy( + await mockUniswapFactory.getAddress(), + await virtualToken.getAddress() + ); + await mockUniswapRouter.waitForDeployment(); + console.log("MockUniswapV2Router02 deployed at:", await mockUniswapRouter.getAddress()); + + // 2.7 Deploy AgentFactoryV6 + console.log("\n--- Deploying AgentFactoryV6 ---"); + const AgentFactoryV6 = await ethers.getContractFactory("AgentFactoryV6"); + const agentFactoryV6 = await upgrades.deployProxy( + AgentFactoryV6, + [ + await agentTokenV2.getAddress(), + await agentVeTokenV2.getAddress(), + await mockAgentDAO.getAddress(), + await mockERC6551Registry.getAddress(), + await virtualToken.getAddress(), + await agentNftV2.getAddress(), + owner.address, // vault + 1, // nextId + ], + { initializer: "initialize" } + ); + await agentFactoryV6.waitForDeployment(); + console.log("AgentFactoryV6 deployed at:", await agentFactoryV6.getAddress()); + + // 2.8 Configure AgentFactoryV6 + console.log("\n--- Configuring AgentFactoryV6 ---"); + await agentFactoryV6.setParams( + 10 * 365 * 24 * 60 * 60, // maturityDuration (10 years) + await mockUniswapRouter.getAddress(), + owner.address, // defaultDelegatee + owner.address // tokenAdmin + ); + console.log("setParams() called for AgentFactoryV6"); + + await agentFactoryV6.setTokenParams(BUY_TAX, SELL_TAX, 1000, owner.address); + console.log("setTokenParams() called for AgentFactoryV6"); + + await agentFactoryV6.grantRole(await agentFactoryV6.DEFAULT_ADMIN_ROLE(), admin.address); + console.log("DEFAULT_ADMIN_ROLE granted to admin in AgentFactoryV6"); + + await agentFactoryV6.grantRole(await agentFactoryV6.REMOVE_LIQUIDITY_ROLE(), admin.address); + console.log("REMOVE_LIQUIDITY_ROLE granted to admin in AgentFactoryV6"); + + // 2.9 Configure AgentNftV2 roles + console.log("\n--- Configuring AgentNftV2 roles ---"); + await agentNftV2.grantRole(await agentNftV2.MINTER_ROLE(), await agentFactoryV6.getAddress()); + console.log("MINTER_ROLE granted to AgentFactoryV6 in AgentNftV2"); + + await agentNftV2.grantRole(await agentNftV2.DEFAULT_ADMIN_ROLE(), admin.address); + console.log("DEFAULT_ADMIN_ROLE granted to admin in AgentNftV2"); + + // ============================================ + // Step 3: Deploy BondingConfig and BondingV5 (like deployLaunchpadv5_3.ts) + // ============================================ + console.log("\n=== Step 3: Deploying BondingConfig and BondingV5 ==="); + + // 3.1 Deploy BondingConfig + console.log("\n--- Deploying BondingConfig ---"); + const BondingConfig = await ethers.getContractFactory("BondingConfig"); + + const scheduledLaunchParams = { + startTimeDelay: START_TIME_DELAY, + normalLaunchFee: NORMAL_LAUNCH_FEE, + acfFee: ACF_FEE, + }; + + const deployParams = { + tbaSalt: TBA_SALT, + tbaImplementation: TBA_IMPLEMENTATION, + daoVotingPeriod: DAO_VOTING_PERIOD, + daoThreshold: DAO_THRESHOLD, + }; + + const bondingCurveParams = { + fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ, + targetRealVirtual: TARGET_REAL_VIRTUAL, + }; + + const bondingConfig = await upgrades.deployProxy( + BondingConfig, + [ + INITIAL_SUPPLY, + owner.address, // feeTo + beOpsWallet.address, // teamTokenReservedWallet + MAX_AIRDROP_PERCENT, // maxAirdropPercent + scheduledLaunchParams, + deployParams, + bondingCurveParams, + ], + { initializer: "initialize" } + ); + await bondingConfig.waitForDeployment(); + console.log("BondingConfig deployed at:", await bondingConfig.getAddress()); + + // 3.2 Deploy BondingV5 + console.log("\n--- Deploying BondingV5 ---"); + const BondingV5 = await ethers.getContractFactory("BondingV5"); + const bondingV5 = await upgrades.deployProxy( + BondingV5, + [ + await fFactoryV2.getAddress(), + await fRouterV2.getAddress(), + await agentFactoryV6.getAddress(), + await bondingConfig.getAddress(), + ], + { initializer: "initialize" } + ); + await bondingV5.waitForDeployment(); + console.log("BondingV5 deployed at:", await bondingV5.getAddress()); + + // 3.3 Grant roles and configure contracts (using owner as deployer has all roles in tests) + console.log("\n--- Granting roles to BondingV5 ---"); + + // Grant CREATOR_ROLE of FFactoryV2 to BondingV5 + await fFactoryV2.grantRole(await fFactoryV2.CREATOR_ROLE(), await bondingV5.getAddress()); + console.log("CREATOR_ROLE granted to BondingV5 in FFactoryV2"); + + // Set BondingV5 and BondingConfig in FRouterV2 (CRITICAL - was missing before) + await fRouterV2.grantRole(await fRouterV2.ADMIN_ROLE(), owner.address); + await fRouterV2.setBondingV5(await bondingV5.getAddress(), await bondingConfig.getAddress()); + console.log("setBondingV5() called in FRouterV2"); + + // Grant EXECUTOR_ROLE of FRouterV2 to BondingV5 + await fRouterV2.grantRole(await fRouterV2.EXECUTOR_ROLE(), await bondingV5.getAddress()); + console.log("EXECUTOR_ROLE granted to BondingV5 in FRouterV2"); + + // Grant BONDING_ROLE of AgentFactoryV6 to BondingV5 + await agentFactoryV6.grantRole(await agentFactoryV6.BONDING_ROLE(), await bondingV5.getAddress()); + console.log("BONDING_ROLE granted to BondingV5 in AgentFactoryV6"); + + // Additional role for admin to call FRouterV2 directly in tests + await fRouterV2.grantRole(await fRouterV2.EXECUTOR_ROLE(), admin.address); + console.log("EXECUTOR_ROLE granted to admin in FRouterV2"); + + // ============================================ + // Step 4: Mint Virtual Tokens to test addresses + // ============================================ + console.log("\n--- Minting Virtual Tokens to test addresses ---"); + const mintAmount = ethers.parseEther("1000000000"); + + const testAddresses = [owner.address, admin.address, beOpsWallet.address, user1.address, user2.address]; + + for (const address of testAddresses) { + await virtualToken.mint(address, mintAmount); + const balance = await virtualToken.balanceOf(address); + console.log(`Minted ${ethers.formatEther(balance)} VT to ${address}`); + } + + // ============================================ + // Store all deployed contracts in setup object + // ============================================ + setup.contracts = { + virtualToken, + fFactoryV2, + fRouterV2, + mockUniswapFactory, + mockUniswapRouter, + agentToken: agentTokenV2, + agentVeToken: agentVeTokenV2, + mockAgentDAO, + mockERC6551Registry, + agentNftV2, + agentFactoryV6, + bondingConfig, + bondingV5, + }; + + setup.accounts = { owner, admin, beOpsWallet, user1, user2 }; + + setup.addresses = { + virtualToken: await virtualToken.getAddress(), + fFactoryV2: await fFactoryV2.getAddress(), + fRouterV2: await fRouterV2.getAddress(), + mockUniswapFactory: await mockUniswapFactory.getAddress(), + mockUniswapRouter: await mockUniswapRouter.getAddress(), + agentToken: await agentTokenV2.getAddress(), + agentVeToken: await agentVeTokenV2.getAddress(), + mockAgentDAO: await mockAgentDAO.getAddress(), + mockERC6551Registry: await mockERC6551Registry.getAddress(), + agentNftV2: await agentNftV2.getAddress(), + agentFactoryV6: await agentFactoryV6.getAddress(), + bondingConfig: await bondingConfig.getAddress(), + bondingV5: await bondingV5.getAddress(), + taxVault: await fFactoryV2.taxVault(), + antiSniperTaxVault: await fFactoryV2.antiSniperTaxVault(), + }; + + setup.params = { + startTimeDelay: START_TIME_DELAY, + daoVotingPeriod: DAO_VOTING_PERIOD, + daoThreshold: DAO_THRESHOLD, + tbaSalt: TBA_SALT, + tbaImplementation: TBA_IMPLEMENTATION, + initialSupply: INITIAL_SUPPLY, + buyTax: BUY_TAX, + sellTax: SELL_TAX, + fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ, + targetRealVirtual: TARGET_REAL_VIRTUAL, + }; + + console.log("\n=== BondingV5 Test Setup Completed Successfully ==="); + console.log("All contracts deployed and configured:"); + console.log("- Virtual Token:", setup.addresses.virtualToken); + console.log("- FFactoryV2:", setup.addresses.fFactoryV2); + console.log("- FRouterV2:", setup.addresses.fRouterV2); + console.log("- AgentFactoryV6:", setup.addresses.agentFactoryV6); + console.log("- BondingConfig:", setup.addresses.bondingConfig); + console.log("- BondingV5:", setup.addresses.bondingV5); + + return setup; + } catch (error) { + console.error("Error during setup:", error); + throw error; + } +} + +describe("BondingV5", function () { + let setup; + let contracts, accounts, addresses, params; + + before(async function () { + setup = await loadFixture(setupBondingV5Test); + contracts = setup.contracts; + accounts = setup.accounts; + addresses = setup.addresses; + params = setup.params; + }); + + describe("Deployment", function () { + it("Should deploy with correct initial parameters", async function () { + const { owner } = accounts; + const { bondingV5, bondingConfig } = contracts; + + expect(await bondingV5.owner()).to.equal(owner.address); + expect(await bondingV5.agentFactory()).to.equal(addresses.agentFactoryV6); + expect(await bondingV5.bondingConfig()).to.equal(addresses.bondingConfig); + + // Check bondingConfig params + expect(await bondingConfig.initialSupply()).to.equal(params.initialSupply); + expect(await bondingConfig.feeTo()).to.equal(owner.address); + }); + + it("Should have correct roles granted", async function () { + const { bondingV5, fRouterV2, agentFactoryV6, fFactoryV2 } = contracts; + + expect( + await fRouterV2.hasRole(await fRouterV2.EXECUTOR_ROLE(), addresses.bondingV5) + ).to.be.true; + + expect( + await agentFactoryV6.hasRole(await agentFactoryV6.BONDING_ROLE(), addresses.bondingV5) + ).to.be.true; + + expect( + await fFactoryV2.hasRole(await fFactoryV2.CREATOR_ROLE(), addresses.bondingV5) + ).to.be.true; + }); + + it("Should have correct bonding curve params configured", async function () { + const { bondingConfig } = contracts; + + const bcParams = await bondingConfig.bondingCurveParams(); + expect(bcParams.fakeInitialVirtualLiq).to.equal(FAKE_INITIAL_VIRTUAL_LIQ); + expect(bcParams.targetRealVirtual).to.equal(TARGET_REAL_VIRTUAL); + }); + }); + + // ============================================ + // LAUNCH_MODE_NORMAL Tests + // ============================================ + describe("LAUNCH_MODE_NORMAL - preLaunch", function () { + it("Should create a new token and application successfully", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const tokenName = "Test Token Normal"; + const tokenTicker = "TESTN"; + const cores = [0, 1, 2]; + const description = "Test token description"; + const image = "https://example.com/image.png"; + const urls = [ + "https://twitter.com/test", + "https://t.me/test", + "https://youtube.com/test", + "https://example.com", + ]; + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + tokenName, + tokenTicker, + cores, + description, + image, + urls, + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, // launchMode_ + 0, // airdropPercent_ + false, // needAcf_ + ANTI_SNIPER_60S, // antiSniperTaxType_ + false // isProject60days_ + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + expect(event).to.not.be.undefined; + + const parsedEvent = bondingV5.interface.parseLog(event); + const tokenAddress = parsedEvent.args.token; + const pairAddress = parsedEvent.args.pair; + + expect(tokenAddress).to.not.equal(ethers.ZeroAddress); + expect(pairAddress).to.not.equal(ethers.ZeroAddress); + + const tokenInfo = await bondingV5.tokenInfo(tokenAddress); + expect(tokenInfo.creator).to.equal(user1.address); + expect(tokenInfo.trading).to.be.true; + expect(tokenInfo.tradingOnUniswap).to.be.false; + + // Verify tokenLaunchParams is stored correctly + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); + expect(launchParams.airdropPercent).to.equal(0); + expect(launchParams.needAcf).to.be.false; + expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_60S); + expect(launchParams.isProject60days).to.be.false; + }); + + it("Should fail with insufficient purchase amount", async function () { + const { user1 } = accounts; + const { bondingV5 } = contracts; + + const purchaseAmount = ethers.parseEther("50"); // Less than fee + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5.connect(user1).preLaunch( + "Test Token", + "TEST", + [0, 1, 2], + "Description", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ) + ).to.be.revertedWithCustomError(bondingV5, ERR_INVALID_INPUT); + }); + + it("Should fail with empty cores array", async function () { + const { user1 } = accounts; + const { bondingV5 } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5.connect(user1).preLaunch( + "Test Token", + "TEST", + [], // Empty cores + "Description", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ) + ).to.be.revertedWithCustomError(bondingV5, ERR_INVALID_INPUT); + }); + + it("Should create token with isProject60days flag", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "Project60days Token", + "P60", + [0, 1, 2], + "Project60days test token", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + true // isProject60days_ = true + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + const parsedEvent = bondingV5.interface.parseLog(event); + const tokenAddress = parsedEvent.args.token; + + // Verify isProject60days returns true + const isProject60days = await bondingV5.isProject60days(tokenAddress); + expect(isProject60days).to.be.true; + }); + }); + + describe("LAUNCH_MODE_NORMAL - launch", function () { + let tokenAddress; + let pairAddress; + + beforeEach(async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "Test Launch Token", + "TLT", + [0, 1, 2], + "Test token description", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + const parsedEvent = bondingV5.interface.parseLog(event); + tokenAddress = parsedEvent.args.token; + pairAddress = parsedEvent.args.pair; + }); + + it("Should launch token successfully", async function () { + const { bondingV5 } = contracts; + + await time.increase(START_TIME_DELAY + 1); + + const tx = await bondingV5.launch(tokenAddress); + const receipt = await tx.wait(); + + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "Launched"; + } catch (e) { + return false; + } + }); + + expect(event).to.not.be.undefined; + const parsedEvent = bondingV5.interface.parseLog(event); + expect(parsedEvent.args.token).to.equal(tokenAddress); + expect(parsedEvent.args.pair).to.equal(pairAddress); + }); + + it("Should fail to launch if start time has not passed", async function () { + const { bondingV5 } = contracts; + + await expect(bondingV5.launch(tokenAddress)).to.be.revertedWithCustomError( + bondingV5, + "InvalidInput" + ); + }); + }); + + describe("LAUNCH_MODE_NORMAL - buy", function () { + let tokenAddress; + + beforeEach(async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "Test Buy Token", + "TBT", + [0, 1, 2], + "Description", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + const parsedEvent = bondingV5.interface.parseLog(event); + tokenAddress = parsedEvent.args.token; + + await time.increase(START_TIME_DELAY + 1); + await bondingV5.launch(tokenAddress); + }); + + it("Should allow buying tokens and bypass anti-sniper tax after 99 minutes", async function () { + const { user2 } = accounts; + const { bondingV5, virtualToken } = contracts; + + await increaseTimeByMinutes(99); + + const buyAmount = ethers.parseEther("100"); + await virtualToken.connect(user2).approve(addresses.fRouterV2, buyAmount); + + const tx = await bondingV5.connect(user2).buy( + buyAmount, + tokenAddress, + 0, + (await time.latest()) + 300 + ); + + expect(tx).to.not.be.undefined; + + const actualTokenContract = await ethers.getContractAt("AgentTokenV2", tokenAddress); + const user2AgentTokenBalance = await actualTokenContract.balanceOf(user2.address); + expect(user2AgentTokenBalance).to.be.greaterThan(0); + }); + }); + + describe("LAUNCH_MODE_NORMAL - sell", function () { + let tokenAddress; + + beforeEach(async function () { + const { user1, user2 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "Test Sell Token", + "TST", + [0, 1, 2], + "Description", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + const parsedEvent = bondingV5.interface.parseLog(event); + tokenAddress = parsedEvent.args.token; + + await time.increase(START_TIME_DELAY + 1); + await bondingV5.launch(tokenAddress); + + // Buy tokens first + await increaseTimeByMinutes(99); + const buyAmount = ethers.parseEther("1000"); + await virtualToken.connect(user2).approve(addresses.fRouterV2, buyAmount); + await bondingV5.connect(user2).buy(buyAmount, tokenAddress, 0, (await time.latest()) + 300); + }); + + it("Should allow selling tokens", async function () { + const { user2 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const actualTokenContract = await ethers.getContractAt("AgentTokenV2", tokenAddress); + const user2AgentTokenBalance = await actualTokenContract.balanceOf(user2.address); + + const sellAmount = user2AgentTokenBalance / 2n; + await actualTokenContract.connect(user2).approve(addresses.fRouterV2, sellAmount); + + const virtualBalanceBefore = await virtualToken.balanceOf(user2.address); + + const tx = await bondingV5.connect(user2).sell( + sellAmount, + tokenAddress, + 0, + (await time.latest()) + 300 + ); + + expect(tx).to.not.be.undefined; + + const virtualBalanceAfter = await virtualToken.balanceOf(user2.address); + expect(virtualBalanceAfter).to.be.greaterThan(virtualBalanceBefore); + }); + }); + + // ============================================ + // PEGASUS_X_LAUNCH Mode Tests + // ============================================ + describe("PEGASUS_X_LAUNCH Mode", function () { + before(async function () { + const { bondingConfig } = contracts; + const { owner, user1 } = accounts; + + // Authorize user1 as XLauncher + await bondingConfig.connect(owner).setXLauncher(user1.address, true); + console.log("user1 authorized as XLauncher for PEGASUS_X_LAUNCH"); + }); + + it("Should create a token with isProjectXLaunch returning true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + // Pegasus modes require immediate launch (startTime within 24h) + const startTime = (await time.latest()) + 100; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "ProjectXLaunch Token", + "PXL", + [0, 1, 2], + "ProjectXLaunch test token", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_X_LAUNCH, + 0, // airdropPercent must be 0 for Pegasus + false, // needAcf must be false for Pegasus + ANTI_SNIPER_NONE, // antiSniperTaxType must be NONE for Pegasus + false // isProject60days must be false for Pegasus + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + expect(event).to.not.be.undefined; + const parsedEvent = bondingV5.interface.parseLog(event); + const tokenAddress = parsedEvent.args.token; + + // Verify isProjectXLaunch returns true + const isProjectXLaunch = await bondingV5.isProjectXLaunch(tokenAddress); + expect(isProjectXLaunch).to.be.true; + + // Verify tokenLaunchParams + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.launchMode).to.equal(LAUNCH_MODE_X_LAUNCH); + }); + + it("Should revert if non-authorized launcher tries to launch X_LAUNCH mode", async function () { + const { user2 } = accounts; + const { bondingV5, virtualToken, bondingConfig } = contracts; + const { owner } = accounts; + + // Ensure user2 is NOT authorized + await bondingConfig.connect(owner).setXLauncher(user2.address, false); + + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user2).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user2).preLaunch( + "Unauthorized X_LAUNCH", + "UXL", + [0, 1, 2], + "Test unauthorized", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_X_LAUNCH, + 0, + false, + ANTI_SNIPER_NONE, + false + ) + ).to.be.revertedWithCustomError(bondingV5, "UnauthorizedLauncher"); + }); + + it("Should revert if Pegasus mode uses invalid params", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + // Should revert with non-zero airdropPercent (even if within maxAirdropPercent) + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid X Launch", + "INV", + [0, 1, 2], + "Test invalid", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_X_LAUNCH, + 5, // airdropPercent = 5 (within maxAirdropPercent but special modes require 0) + false, + ANTI_SNIPER_NONE, + false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + }); + + // ============================================ + // PEGASUS_ACP_SKILL Mode Tests + // ============================================ + describe("PEGASUS_ACP_SKILL Mode", function () { + before(async function () { + const { bondingConfig } = contracts; + const { owner, user1 } = accounts; + + // Authorize user1 as AcpSkillLauncher + await bondingConfig.connect(owner).setAcpSkillLauncher(user1.address, true); + console.log("user1 authorized as AcpSkillLauncher for PEGASUS_ACP_SKILL"); + }); + + it("Should create a token with isAcpSkillLaunch returning true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "AcpSkillLaunch Token", + "ACPS", + [0, 1, 2], + "AcpSkillLaunch test token", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_ACP_SKILL, + 0, + false, + ANTI_SNIPER_NONE, + false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + expect(event).to.not.be.undefined; + const parsedEvent = bondingV5.interface.parseLog(event); + const tokenAddress = parsedEvent.args.token; + + // Verify isAcpSkillLaunch returns true + const isAcpSkillLaunch = await bondingV5.isAcpSkillLaunch(tokenAddress); + expect(isAcpSkillLaunch).to.be.true; + + // Verify tokenLaunchParams + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.launchMode).to.equal(LAUNCH_MODE_ACP_SKILL); + }); + + it("Should revert if non-authorized launcher tries to launch ACP_SKILL mode", async function () { + const { user2 } = accounts; + const { bondingV5, virtualToken, bondingConfig } = contracts; + const { owner } = accounts; + + // Ensure user2 is NOT authorized + await bondingConfig.connect(owner).setAcpSkillLauncher(user2.address, false); + + const purchaseAmount = ethers.parseEther("1000"); + + await virtualToken.connect(user2).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user2).preLaunch( + "Unauthorized ACP_SKILL", + "UACP", + [0, 1, 2], + "Test unauthorized", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_ACP_SKILL, + 0, + false, + ANTI_SNIPER_NONE, + false + ) + ).to.be.revertedWithCustomError(bondingV5, "UnauthorizedLauncher"); + }); + }); + + // ============================================ + // BondingConfig Admin Tests + // ============================================ + describe("BondingConfig Admin Functions", function () { + it("Should allow owner to update scheduled launch params", async function () { + const { owner } = accounts; + const { bondingConfig } = contracts; + + const newParams = { + startTimeDelay: START_TIME_DELAY * 2, + normalLaunchFee: ethers.parseEther("200"), + acfFee: ethers.parseEther("50"), + }; + + await bondingConfig.connect(owner).setScheduledLaunchParams(newParams); + + const params = await bondingConfig.scheduledLaunchParams(); + expect(params.normalLaunchFee).to.equal(newParams.normalLaunchFee); + expect(params.acfFee).to.equal(newParams.acfFee); + + // Reset to original + await bondingConfig.connect(owner).setScheduledLaunchParams({ + startTimeDelay: START_TIME_DELAY, + normalLaunchFee: NORMAL_LAUNCH_FEE, + acfFee: ACF_FEE, + }); + }); + + it("Should allow owner to update bonding curve params", async function () { + const { owner } = accounts; + const { bondingConfig } = contracts; + + const newParams = { + fakeInitialVirtualLiq: ethers.parseEther("7000"), + targetRealVirtual: ethers.parseEther("50000"), + }; + + await bondingConfig.connect(owner).setBondingCurveParams(newParams); + + const params = await bondingConfig.bondingCurveParams(); + expect(params.fakeInitialVirtualLiq).to.equal(newParams.fakeInitialVirtualLiq); + expect(params.targetRealVirtual).to.equal(newParams.targetRealVirtual); + + // Reset to original + await bondingConfig.connect(owner).setBondingCurveParams({ + fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ, + targetRealVirtual: TARGET_REAL_VIRTUAL, + }); + }); + + it("Should revert if non-owner tries to update params", async function () { + const { user1 } = accounts; + const { bondingConfig } = contracts; + + const newParams = { + startTimeDelay: START_TIME_DELAY * 2, + normalLaunchFee: ethers.parseEther("200"), + acfFee: ethers.parseEther("50"), + }; + + await expect( + bondingConfig.connect(user1).setScheduledLaunchParams(newParams) + ).to.be.revertedWithCustomError(bondingConfig, "OwnableUnauthorizedAccount"); + }); + + it("Should allow owner to set and revoke XLauncher", async function () { + const { owner, user2 } = accounts; + const { bondingConfig } = contracts; + + // Initially user2 should not be authorized + expect(await bondingConfig.isXLauncher(user2.address)).to.be.false; + + // Authorize user2 + await bondingConfig.connect(owner).setXLauncher(user2.address, true); + expect(await bondingConfig.isXLauncher(user2.address)).to.be.true; + + // Revoke authorization + await bondingConfig.connect(owner).setXLauncher(user2.address, false); + expect(await bondingConfig.isXLauncher(user2.address)).to.be.false; + }); + + it("Should allow owner to set and revoke AcpSkillLauncher", async function () { + const { owner, user2 } = accounts; + const { bondingConfig } = contracts; + + // Initially user2 should not be authorized + expect(await bondingConfig.isAcpSkillLauncher(user2.address)).to.be.false; + + // Authorize user2 + await bondingConfig.connect(owner).setAcpSkillLauncher(user2.address, true); + expect(await bondingConfig.isAcpSkillLauncher(user2.address)).to.be.true; + + // Revoke authorization + await bondingConfig.connect(owner).setAcpSkillLauncher(user2.address, false); + expect(await bondingConfig.isAcpSkillLauncher(user2.address)).to.be.false; + }); + }); + + // ============================================ + // Anti-Sniper Tax Type Tests + // ============================================ + describe("Anti-Sniper Tax Types", function () { + it("Should validate anti-sniper tax types correctly", async function () { + const { bondingConfig } = contracts; + + expect(await bondingConfig.isValidAntiSniperType(ANTI_SNIPER_NONE)).to.be.true; + expect(await bondingConfig.isValidAntiSniperType(ANTI_SNIPER_60S)).to.be.true; + expect(await bondingConfig.isValidAntiSniperType(ANTI_SNIPER_98M)).to.be.true; + expect(await bondingConfig.isValidAntiSniperType(3)).to.be.false; + }); + + it("Should return correct durations for anti-sniper types", async function () { + const { bondingConfig } = contracts; + + expect(await bondingConfig.getAntiSniperDuration(ANTI_SNIPER_NONE)).to.equal(0); + expect(await bondingConfig.getAntiSniperDuration(ANTI_SNIPER_60S)).to.equal(60); + expect(await bondingConfig.getAntiSniperDuration(ANTI_SNIPER_98M)).to.equal(98 * 60); + }); + + it("Should revert preLaunch with invalid anti-sniper type", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid Anti Sniper", + "IAS", + [0, 1, 2], + "Description", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + 5, // Invalid anti-sniper type + false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidAntiSniperType"); + }); + }); + + // ============================================ + // Comprehensive Permutation Tests for New Configurable Options + // ============================================ + describe("Configurable Options Permutations", function () { + + describe("airdropPercent Variations", function () { + it("Should create token with 0% airdrop", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Zero Airdrop Token", "ZAT", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.airdropPercent).to.equal(0); + }); + + it("Should create token with max airdrop (5%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Max Airdrop Token", "T5", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false // MAX_AIRDROP_PERCENT = 5 + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.airdropPercent).to.equal(5); + }); + + it("Should create token with 3% airdrop", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "3% Airdrop Token", "T3", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 3, false, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.airdropPercent).to.equal(3); + }); + + it("Should revert with airdropPercent exceeding MAX_AIRDROP_PERCENT (6% > 5%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken, bondingConfig } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5.connect(user1).preLaunch( + "Exceed Airdrop", "EXC", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 6, false, ANTI_SNIPER_60S, false // 6% > MAX_AIRDROP_PERCENT (5%) + ) + ).to.be.revertedWithCustomError(bondingConfig, "AirdropPercentExceedsMax"); + }); + }); + + describe("needAcf Variations", function () { + it("Should create token with needAcf = true and charge fee", async function () { + const { user1, owner } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const feeToBalanceBefore = await virtualToken.balanceOf(owner.address); + + // Immediate launch with needAcf = true should charge fee + const startTime = (await time.latest()) + 100; + const tx = await bondingV5.connect(user1).preLaunch( + "ACF Token", "ACF", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, true, ANTI_SNIPER_60S, false // needAcf = true + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.needAcf).to.be.true; + + // Fee should be charged + const feeToBalanceAfter = await virtualToken.balanceOf(owner.address); + expect(feeToBalanceAfter).to.be.greaterThan(feeToBalanceBefore); + }); + + it("Should create token with needAcf = false and not charge fee for immediate launch", async function () { + const { user1, owner } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const feeToBalanceBefore = await virtualToken.balanceOf(owner.address); + + // Immediate launch without ACF should not charge fee + const startTime = (await time.latest()) + 100; + const tx = await bondingV5.connect(user1).preLaunch( + "No ACF Token", "NACF", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false // needAcf = false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.needAcf).to.be.false; + + // Fee should NOT be charged for immediate launch without ACF + const feeToBalanceAfter = await virtualToken.balanceOf(owner.address); + expect(feeToBalanceAfter).to.equal(feeToBalanceBefore); + }); + + it("Should revert if needAcf = true and airdropPercent causes total to exceed limit", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken, bondingConfig } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + // needAcf = true adds 50% reserve, so total = 5 + 50 = 55 >= MAX_TOTAL_RESERVED_PERCENT (55) + await expect( + bondingV5.connect(user1).preLaunch( + "ACF Exceed", "ACFE", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, true, ANTI_SNIPER_60S, false + ) + ).to.be.revertedWithCustomError(bondingConfig, "InvalidReservePercent"); + }); + + it("Should allow needAcf = true with airdropPercent = 5 (total 55%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + // needAcf = true adds 50% reserve, so total = 4 + 50 = 54 < 55 OK + const tx = await bondingV5.connect(user1).preLaunch( + "ACF With Airdrop", "ACFA", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + expect(event).to.not.be.undefined; + }); + }); + + describe("Anti-Sniper Tax Type Variations", function () { + it("Should create token with ANTI_SNIPER_NONE (0s duration)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "No Anti-Sniper", "NOAS", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_NONE, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + expect(await bondingV5.tokenAntiSniperType(tokenAddress)).to.equal(ANTI_SNIPER_NONE); + }); + + it("Should create token with ANTI_SNIPER_60S (60s duration)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "60s Anti-Sniper", "AS60", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + expect(await bondingV5.tokenAntiSniperType(tokenAddress)).to.equal(ANTI_SNIPER_60S); + }); + + it("Should create token with ANTI_SNIPER_98M (98 minutes duration)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "98m Anti-Sniper", "AS98", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_98M, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + expect(await bondingV5.tokenAntiSniperType(tokenAddress)).to.equal(ANTI_SNIPER_98M); + }); + }); + + describe("isProject60days Variations", function () { + it("Should create token with isProject60days = true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Project 60days", "P60D", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, true // isProject60days = true + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + expect(await bondingV5.isProject60days(tokenAddress)).to.be.true; + }); + + it("Should create token with isProject60days = false", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Regular Project", "REG", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false // isProject60days = false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + expect(await bondingV5.isProject60days(tokenAddress)).to.be.false; + }); + }); + + describe("Scheduled vs Immediate Launch", function () { + it("Should charge fee for scheduled launch (startTime >= now + 24h)", async function () { + const { user1, owner } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const feeToBalanceBefore = await virtualToken.balanceOf(owner.address); + + // Scheduled launch (startTime > now + 24h) + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Scheduled Token", "SCHD", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + expect(receipt).to.not.be.undefined; + + // Fee should be charged for scheduled launch + const feeToBalanceAfter = await virtualToken.balanceOf(owner.address); + expect(feeToBalanceAfter).to.be.greaterThan(feeToBalanceBefore); + }); + + it("Should NOT charge fee for immediate launch without ACF", async function () { + const { user1, owner } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const feeToBalanceBefore = await virtualToken.balanceOf(owner.address); + + // Immediate launch (startTime < now + 24h) + const startTime = (await time.latest()) + 100; + const tx = await bondingV5.connect(user1).preLaunch( + "Immediate Token", "IMMD", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + expect(receipt).to.not.be.undefined; + + // Fee should NOT be charged for immediate launch without ACF + const feeToBalanceAfter = await virtualToken.balanceOf(owner.address); + expect(feeToBalanceAfter).to.equal(feeToBalanceBefore); + }); + }); + }); + + // ============================================ + // Pegasus Mode Strict Validation Tests + // ============================================ + describe("Pegasus Mode Strict Validation", function () { + before(async function () { + const { bondingConfig } = contracts; + const { owner, user1 } = accounts; + + await bondingConfig.connect(owner).setXLauncher(user1.address, true); + await bondingConfig.connect(owner).setAcpSkillLauncher(user1.address, true); + }); + + it("Should revert Pegasus X_LAUNCH with non-zero airdropPercent", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid Pegasus", "INV", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_X_LAUNCH, 5, false, ANTI_SNIPER_NONE, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert Pegasus X_LAUNCH with needAcf = true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid Pegasus ACF", "INVA", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_X_LAUNCH, 0, true, ANTI_SNIPER_NONE, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert Pegasus X_LAUNCH with non-NONE anti-sniper type", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid Pegasus AS", "INAS", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_X_LAUNCH, 0, false, ANTI_SNIPER_60S, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert Pegasus X_LAUNCH with isProject60days = true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid Pegasus 60D", "IN60", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_X_LAUNCH, 0, false, ANTI_SNIPER_NONE, true + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert Pegasus X_LAUNCH with scheduled launch (not immediate)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + // Scheduled launch (startTime >= now + 24h) + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5.connect(user1).preLaunch( + "Scheduled Pegasus", "SCHP", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_X_LAUNCH, 0, false, ANTI_SNIPER_NONE, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert Pegasus ACP_SKILL with any invalid param combination", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + // Test all invalid params for ACP_SKILL + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid ACP", "IACP", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_ACP_SKILL, 5, false, ANTI_SNIPER_NONE, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + }); + + // ============================================ + // Event Data Verification Tests + // ============================================ + describe("Event Data Verification", function () { + it("Should emit PreLaunched event with correct LaunchParams struct", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Event Test Token", "EVT", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_98M, true // Use MAX_AIRDROP_PERCENT (5) + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + + const parsedEvent = bondingV5.interface.parseLog(event); + + // Verify LaunchParams in event + const launchParams = parsedEvent.args.launchParams; + expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); + expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.needAcf).to.equal(false); + expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); + expect(launchParams.isProject60days).to.equal(true); + }); + + it("Should emit Launched event with correct LaunchParams struct", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + let tx = await bondingV5.connect(user1).preLaunch( + "Launch Event Token", "LET", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false + ); + + let receipt = await tx.wait(); + let event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + // Wait and launch + await time.increase(START_TIME_DELAY + 1); + tx = await bondingV5.launch(tokenAddress); + receipt = await tx.wait(); + + event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "Launched"; } + catch (e) { return false; } + }); + + const parsedEvent = bondingV5.interface.parseLog(event); + + // Verify LaunchParams in Launched event + const launchParams = parsedEvent.args.launchParams; + expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); + expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.needAcf).to.equal(false); + expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_60S); + expect(launchParams.isProject60days).to.equal(false); + }); + }); + + // ============================================ + // Token Graduation Threshold Tests + // ============================================ + describe("Token Graduation Threshold Calculation", function () { + it("Should calculate different gradThreshold for different airdropPercent", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + + // Token 1: 0% airdrop + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + let startTime = (await time.latest()) + START_TIME_DELAY + 1; + let tx = await bondingV5.connect(user1).preLaunch( + "0% Airdrop Grad", "G0", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false + ); + let receipt = await tx.wait(); + let event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const token1 = bondingV5.interface.parseLog(event).args.token; + const gradThreshold1 = await bondingV5.tokenGradThreshold(token1); + + // Token 2: 5% airdrop (MAX_AIRDROP_PERCENT) + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + startTime = (await time.latest()) + START_TIME_DELAY + 1; + tx = await bondingV5.connect(user1).preLaunch( + "5% Airdrop Grad", "G5", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false + ); + receipt = await tx.wait(); + event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const token2 = bondingV5.interface.parseLog(event).args.token; + const gradThreshold2 = await bondingV5.tokenGradThreshold(token2); + + // Different airdrop should result in different graduation thresholds + expect(gradThreshold1).to.not.equal(gradThreshold2); + // Higher airdrop means less bonding curve supply, so lower gradThreshold + expect(gradThreshold2).to.be.lessThan(gradThreshold1); + }); + + it("Should calculate gradThreshold with needAcf = true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + + // Token with ACF (adds 50% reserve) and 0% airdrop + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "ACF Token Grad", "GACF", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, true, ANTI_SNIPER_60S, false + ); + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const gradThreshold = await bondingV5.tokenGradThreshold(tokenAddress); + expect(gradThreshold).to.be.greaterThan(0); + }); + }); + + // ============================================ + // Edge Cases and Boundary Tests + // ============================================ + describe("Edge Cases and Boundary Tests", function () { + it("Should revert with invalid launch mode (mode = 3)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid Mode", "INV", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + 3, // Invalid launch mode + 0, false, ANTI_SNIPER_60S, false + ) + ).to.be.revertedWithCustomError(bondingV5, "LaunchModeNotEnabled"); + }); + + it("Should allow exact boundary of MAX_TOTAL_RESERVED_PERCENT (needAcf + 4% = 54%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + // needAcf (50%) + 4% = 54% should work (just under 55% limit) + const tx = await bondingV5.connect(user1).preLaunch( + "Boundary Test", "BNDY", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + expect(receipt).to.not.be.undefined; + }); + + it("Should revert at exact MAX_TOTAL_RESERVED_PERCENT (needAcf + 5% = 55%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken, bondingConfig } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + // needAcf (50%) + 5% = 55% should fail (at limit) + await expect( + bondingV5.connect(user1).preLaunch( + "Over Limit", "OVER", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, true, ANTI_SNIPER_60S, false + ) + ).to.be.revertedWithCustomError(bondingConfig, "InvalidReservePercent"); + }); + + it("Should allow needAcf = true with 0% airdrop (total 50%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + const tx = await bondingV5.connect(user1).preLaunch( + "ACF Only", "ACFO", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, true, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.needAcf).to.be.true; + expect(launchParams.airdropPercent).to.equal(0); + }); + + it("Should allow exactly 4% airdrop + ACF (total 54%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + // 4% + 50% (ACF) = 54% < 55% limit + const tx = await bondingV5.connect(user1).preLaunch( + "Max ACF Combo", "MXAC", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + expect(receipt).to.not.be.undefined; + }); + + it("Should revert 5% airdrop + ACF (total 55%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken, bondingConfig } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + // 5% + 50% (ACF) = 55% >= 55% limit + await expect( + bondingV5.connect(user1).preLaunch( + "Over ACF Combo", "OVAC", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, true, ANTI_SNIPER_60S, false + ) + ).to.be.revertedWithCustomError(bondingConfig, "InvalidReservePercent"); + }); + }); + + // ============================================ + // Full Parameter Combination Tests + // ============================================ + describe("Full Parameter Combination Tests", function () { + it("Should create token with all parameters at default values", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Default Params", "DFLT", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_NONE, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); + expect(launchParams.airdropPercent).to.equal(0); + expect(launchParams.needAcf).to.equal(false); + expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_NONE); + expect(launchParams.isProject60days).to.equal(false); + }); + + it("Should create token with maximum allowed parameters", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Max Params", "MAXP", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_98M, true // MAX_AIRDROP_PERCENT = 5 + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); + expect(launchParams.isProject60days).to.equal(true); + }); + + it("Should handle multiple tokens with different parameter combinations", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + const tokens = []; + + // Test matrix of parameter combinations (airdrop values <= MAX_AIRDROP_PERCENT = 5) + const testCases = [ + { airdrop: 0, needAcf: false, antiSniper: ANTI_SNIPER_NONE, is60days: false }, + { airdrop: 3, needAcf: false, antiSniper: ANTI_SNIPER_60S, is60days: true }, + { airdrop: 5, needAcf: false, antiSniper: ANTI_SNIPER_98M, is60days: false }, + { airdrop: 0, needAcf: true, antiSniper: ANTI_SNIPER_60S, is60days: false }, + { airdrop: 4, needAcf: true, antiSniper: ANTI_SNIPER_98M, is60days: true }, + ]; + + for (let i = 0; i < testCases.length; i++) { + const tc = testCases[i]; + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + const tx = await bondingV5.connect(user1).preLaunch( + `Combo Token ${i}`, `CMB${i}`, [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, tc.airdrop, tc.needAcf, tc.antiSniper, tc.is60days + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + tokens.push(tokenAddress); + + // Verify stored parameters + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.airdropPercent).to.equal(tc.airdrop); + expect(launchParams.needAcf).to.equal(tc.needAcf); + expect(launchParams.antiSniperTaxType).to.equal(tc.antiSniper); + expect(launchParams.isProject60days).to.equal(tc.is60days); + } + + // Verify all tokens were created with unique addresses + expect(new Set(tokens).size).to.equal(testCases.length); + }); + }); + + // ============================================ + // Regression Tests + // ============================================ + describe("Regression Tests", function () { + it("Should maintain token info after preLaunch", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Regression Token", "REGT", [0, 1, 2], "A regression test token", + "https://example.com/image.png", ["url1", "url2", "url3", "url4"], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, true // Use MAX_AIRDROP_PERCENT (5) + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + // Verify token info from BondingV5 + const tokenInfo = await bondingV5.tokenInfo(tokenAddress); + expect(tokenInfo.creator).to.equal(user1.address); + expect(tokenInfo.description).to.equal("A regression test token"); + expect(tokenInfo.image).to.equal("https://example.com/image.png"); + expect(tokenInfo.trading).to.be.true; // Bonding curve trading is active after preLaunch + expect(tokenInfo.launchExecuted).to.be.false; // Launch() not yet called + }); + + it("Should correctly transition from preLaunch to launch", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Transition Token", "TRAN", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + // Check state after preLaunch (before launch() call) + let tokenInfo = await bondingV5.tokenInfo(tokenAddress); + expect(tokenInfo.trading).to.be.true; // Bonding curve trading active + expect(tokenInfo.launchExecuted).to.be.false; // launch() not yet called + + // Wait and call launch() + await time.increase(START_TIME_DELAY + 1); + await bondingV5.launch(tokenAddress); + + // Check state after launch() + tokenInfo = await bondingV5.tokenInfo(tokenAddress); + expect(tokenInfo.trading).to.be.true; // Still trading on bonding curve + expect(tokenInfo.launchExecuted).to.be.true; // Launch executed + }); + + it("Should preserve launch params after launch", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Preserve Token", "PRSV", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_98M, true // Use MAX_AIRDROP_PERCENT (5) + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + // Wait and launch + await time.increase(START_TIME_DELAY + 1); + await bondingV5.launch(tokenAddress); + + // Verify launch params are still correct after launch + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.needAcf).to.equal(false); + expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); + expect(launchParams.isProject60days).to.equal(true); + }); + }); +}); From 5c75ff5accaac06714bf10d1416c4f515571d494 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Tue, 10 Mar 2026 20:05:02 +0800 Subject: [PATCH 2/8] cater for general multi-chain bonding support and fix deployment scripts --- .openzeppelin/sepolia.json | 1337 +++++++++++++++++- contracts/launchpadv2/BondingConfig.sol | 74 +- contracts/launchpadv2/BondingV5.sol | 300 ++-- contracts/tax/AgentTax.sol | 75 +- hardhat.config.js | 8 +- scripts/launchpadv5/AUDIT_SUMMARY.md | 450 ++++++ scripts/launchpadv5/deployLaunchpadv5_0.ts | 47 +- scripts/launchpadv5/deployLaunchpadv5_1.ts | 28 +- scripts/launchpadv5/deployLaunchpadv5_2.ts | 28 +- scripts/launchpadv5/deployLaunchpadv5_3.ts | 25 + scripts/launchpadv5/deploy_agent_tax_swap.ts | 331 +++++ scripts/launchpadv5/handle_agent_tax.ts | 180 +++ scripts/launchpadv5/utils.ts | 22 + test/launchpadv5/bondingV5.js | 657 ++++++++- 14 files changed, 3279 insertions(+), 283 deletions(-) create mode 100644 scripts/launchpadv5/AUDIT_SUMMARY.md create mode 100644 scripts/launchpadv5/deploy_agent_tax_swap.ts create mode 100644 scripts/launchpadv5/handle_agent_tax.ts create mode 100644 scripts/launchpadv5/utils.ts diff --git a/.openzeppelin/sepolia.json b/.openzeppelin/sepolia.json index bae9650..d54cb21 100644 --- a/.openzeppelin/sepolia.json +++ b/.openzeppelin/sepolia.json @@ -60,6 +60,51 @@ "address": "0x06dcB8126e3E060c0272A5cDBd1a00Df4843f42e", "txHash": "0x30fb8ca12ed869334caac318274fd01012b1c57b370251704574f9099169ce9b", "kind": "transparent" + }, + { + "address": "0xc06FcA20f76e99e187cA37A82D8927AE263eec21", + "txHash": "0x7e8decd623bf4f6e695a00dfdc53895632d3be1573f83415ab85406842ce08da", + "kind": "transparent" + }, + { + "address": "0x4C45567Bd7Ba6c06ed45D50eeE59d2703805F1c2", + "txHash": "0xe7e5d4d06cff1d7866b11750322863e26690f85cba27607e11312d6886976149", + "kind": "transparent" + }, + { + "address": "0xd7CF2ea711B843226Eb6C31b8C07141dd1a54380", + "txHash": "0xa360e8d9591fdfc364438554203f514ba8753135e3b30c550218ddf70fdc8e46", + "kind": "transparent" + }, + { + "address": "0x8caf2CcDA05d6cBc9531cc9E66a86A5C5AEe4fa7", + "txHash": "0x8c6dfdd4a846f164ef4e6b68e84ebc10f662187d3ecd34007419e7a718de125e", + "kind": "transparent" + }, + { + "address": "0x2970c9c0020FB03b71ca711aAB360aD36B89C8d4", + "txHash": "0x6d45a385c93d80892a499c3113eaa1f1474e0178738435ae7504a13f9ec49c96", + "kind": "transparent" + }, + { + "address": "0x7cF0e4c285E29644a63a2Ce106442038B3DAAfbB", + "txHash": "0xf7f20e02a355b6b9977e042fe358e113dd7b1674f81c692feca26a91fc62a6bf", + "kind": "transparent" + }, + { + "address": "0x365E9A6C64D95BDa9a2B3356e94E2f8288f88970", + "txHash": "0x56ca8ea701357b4707eaa328ac116269063f657984a9f106aebd8501e893d5e0", + "kind": "transparent" + }, + { + "address": "0xEfE9188266fE570C6Cb9504570538d1723E2C144", + "txHash": "0x39a828b6242080271e8c58763535671996adbb320169d74282cfe90c84a933c0", + "kind": "transparent" + }, + { + "address": "0x533F923dd5300e4fc821BE8FBD27a2baF060d7B9", + "txHash": "0x3099bc673961bcd62f797cbe3d38958e0f084674055e3f5fff6c670d56b32aad", + "kind": "transparent" } ], "impls": { @@ -2355,7 +2400,7 @@ "label": "router", "offset": 0, "slot": "2", - "type": "t_contract(IRouter)51611", + "type": "t_contract(IRouter)54273", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:45" }, @@ -2395,7 +2440,7 @@ "label": "agentNft", "offset": 0, "slot": "6", - "type": "t_contract(IAgentNft)72707", + "type": "t_contract(IAgentNft)75369", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:50" }, @@ -2411,7 +2456,7 @@ "label": "taxHistory", "offset": 0, "slot": "8", - "type": "t_mapping(t_bytes32,t_struct(TaxHistory)53360_storage)", + "type": "t_mapping(t_bytes32,t_struct(TaxHistory)56022_storage)", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:68" }, @@ -2419,7 +2464,7 @@ "label": "agentTaxAmounts", "offset": 0, "slot": "9", - "type": "t_mapping(t_uint256,t_struct(TaxAmounts)53365_storage)", + "type": "t_mapping(t_uint256,t_struct(TaxAmounts)56027_storage)", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:69" }, @@ -2427,7 +2472,7 @@ "label": "_agentRecipients", "offset": 0, "slot": "10", - "type": "t_mapping(t_uint256,t_struct(TaxRecipient)53467_storage)", + "type": "t_mapping(t_uint256,t_struct(TaxRecipient)56129_storage)", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:88" }, @@ -2443,7 +2488,7 @@ "label": "tbaBonus", "offset": 2, "slot": "11", - "type": "t_contract(ITBABonus)55297", + "type": "t_contract(ITBABonus)57959", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:97" }, @@ -2451,7 +2496,7 @@ "label": "bondingV2", "offset": 0, "slot": "12", - "type": "t_contract(IBondingV2ForTax)53332", + "type": "t_contract(IBondingV2ForTax)55994", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:98" }, @@ -2459,7 +2504,7 @@ "label": "bondingV4", "offset": 0, "slot": "13", - "type": "t_contract(IBondingV4ForTax)53347", + "type": "t_contract(IBondingV4ForTax)56009", "contract": "AgentTax", "src": "contracts/tax/AgentTax.sol:99" } @@ -2537,27 +2582,1281 @@ "label": "uint64", "numberOfBytes": "8" }, - "t_contract(IAgentNft)72707": { + "t_contract(IAgentNft)75369": { "label": "contract IAgentNft", "numberOfBytes": "20" }, - "t_contract(IBondingV2ForTax)53332": { + "t_contract(IBondingV2ForTax)55994": { "label": "contract IBondingV2ForTax", "numberOfBytes": "20" }, - "t_contract(IBondingV4ForTax)53347": { + "t_contract(IBondingV4ForTax)56009": { "label": "contract IBondingV4ForTax", "numberOfBytes": "20" }, - "t_contract(IRouter)51611": { + "t_contract(IRouter)54273": { + "label": "contract IRouter", + "numberOfBytes": "20" + }, + "t_contract(ITBABonus)57959": { + "label": "contract ITBABonus", + "numberOfBytes": "20" + }, + "t_mapping(t_bytes32,t_struct(TaxHistory)56022_storage)": { + "label": "mapping(bytes32 => struct AgentTax.TaxHistory)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_address)": { + "label": "mapping(uint256 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(TaxAmounts)56027_storage)": { + "label": "mapping(uint256 => struct AgentTax.TaxAmounts)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_struct(TaxRecipient)56129_storage)": { + "label": "mapping(uint256 => struct AgentTax.TaxRecipient)", + "numberOfBytes": "32" + }, + "t_struct(TaxAmounts)56027_storage": { + "label": "struct AgentTax.TaxAmounts", + "members": [ + { + "label": "amountCollected", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amountSwapped", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TaxHistory)56022_storage": { + "label": "struct AgentTax.TaxHistory", + "members": [ + { + "label": "agentId", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "amount", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(TaxRecipient)56129_storage": { + "label": "struct AgentTax.TaxRecipient", + "members": [ + { + "label": "tba", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "36d26e316ace60c7c0b5ca62a259eba8e6ec1f7ea6469691a9724daf05311d63": { + "address": "0xDd217140FAb94B5dAEf177c39cF6288FEdC08654", + "txHash": "0x9a7ab0e00701b2b6792a7e6a93da4f697219d15c4d9dc43ba5e11b595030f095", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(FFactoryV2)45561", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:40" + }, + { + "label": "assetToken", + "offset": 0, + "slot": "1", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:41" + }, + { + "label": "taxManager", + "offset": 0, + "slot": "2", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:42" + }, + { + "label": "antiSniperTaxManager", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:43" + }, + { + "label": "bondingV4", + "offset": 0, + "slot": "4", + "type": "t_contract(IBondingV4ForRouter)46080", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:46" + }, + { + "label": "bondingV5", + "offset": 0, + "slot": "5", + "type": "t_contract(IBondingV5ForRouter)46088", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:49" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "6", + "type": "t_contract(IBondingConfigForRouter)46101", + "contract": "FRouterV2", + "src": "contracts/launchpadv2/FRouterV2.sol:50" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)1512_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)2531_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_contract(FFactoryV2)45561": { + "label": "contract FFactoryV2", + "numberOfBytes": "20" + }, + "t_contract(IBondingConfigForRouter)46101": { + "label": "contract IBondingConfigForRouter", + "numberOfBytes": "20" + }, + "t_contract(IBondingV4ForRouter)46080": { + "label": "contract IBondingV4ForRouter", + "numberOfBytes": "20" + }, + "t_contract(IBondingV5ForRouter)46088": { + "label": "contract IBondingV5ForRouter", + "numberOfBytes": "20" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "5c02f82682cd74435875189a2130c8a7604448a37b10eb1e37148c81869ea731": { + "address": "0x38f52498D78C6BC23D1B91d3BB2Fd161695a6f38", + "txHash": "0x7db0270be418932934cbf68ffec5330393d57a720dd2a9253069c7a7fca9d0a1", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "_scheduledLaunchParams", + "offset": 0, + "slot": "0", + "type": "t_struct(ScheduledLaunchParams)1346_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:38" + }, + { + "label": "teamTokenReservedWallet", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:74" + }, + { + "label": "maxAirdropPercent", + "offset": 20, + "slot": "3", + "type": "t_uint8", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:77" + }, + { + "label": "isXLauncher", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:80" + }, + { + "label": "isAcpSkillLauncher", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:81" + }, + { + "label": "bondingCurveParams", + "offset": 0, + "slot": "6", + "type": "t_struct(BondingCurveParams)1408_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:88" + }, + { + "label": "_deployParams", + "offset": 0, + "slot": "8", + "type": "t_struct(DeployParams)1495_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:144" + }, + { + "label": "initialSupply", + "offset": 0, + "slot": "11", + "type": "t_uint256", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:151" + }, + { + "label": "feeTo", + "offset": 0, + "slot": "12", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:152" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_struct(BondingCurveParams)1408_storage": { + "label": "struct BondingConfig.BondingCurveParams", + "members": [ + { + "label": "fakeInitialVirtualLiq", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "targetRealVirtual", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(DeployParams)1495_storage": { + "label": "struct BondingConfig.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ScheduledLaunchParams)1346_storage": { + "label": "struct BondingConfig.ScheduledLaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "normalLaunchFee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "acfFee", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "0e720901e0b9c0729eef6dbb079a4850508dfe038219365f4af1eb3d448b6953": { + "address": "0xe916DCF7D07b090194A0d31c259F5719A1a12735", + "txHash": "0xf2d7591be80553554ff66f42c5ebf9a091b2be28b27b883a24ca308865a25424", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(IFFactoryV2Minimal)1942", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:68" + }, + { + "label": "router", + "offset": 0, + "slot": "1", + "type": "t_contract(IFRouterV2Minimal)2004", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:69" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "2", + "type": "t_contract(IAgentFactoryV6Minimal)2069", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:70" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "3", + "type": "t_contract(BondingConfig)1909", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:71" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_struct(Token)1475_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:73" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "5", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:74" + }, + { + "label": "tokenLaunchParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_struct(LaunchParams)1486_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:80" + }, + { + "label": "tokenGradThreshold", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:83" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)158_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_contract(BondingConfig)1909": { + "label": "contract BondingConfig", + "numberOfBytes": "20" + }, + "t_contract(IAgentFactoryV6Minimal)2069": { + "label": "contract IAgentFactoryV6Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFFactoryV2Minimal)1942": { + "label": "contract IFFactoryV2Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFRouterV2Minimal)2004": { + "label": "contract IFRouterV2Minimal", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(LaunchParams)1486_storage)": { + "label": "mapping(address => struct BondingConfig.LaunchParams)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)1475_storage)": { + "label": "mapping(address => struct BondingConfig.Token)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)1436_storage": { + "label": "struct BondingConfig.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(LaunchParams)1486_storage": { + "label": "struct BondingConfig.LaunchParams", + "members": [ + { + "label": "launchMode", + "type": "t_uint8", + "offset": 0, + "slot": "0" + }, + { + "label": "airdropPercent", + "type": "t_uint8", + "offset": 1, + "slot": "0" + }, + { + "label": "needAcf", + "type": "t_bool", + "offset": 2, + "slot": "0" + }, + { + "label": "antiSniperTaxType", + "type": "t_uint8", + "offset": 3, + "slot": "0" + }, + { + "label": "isProject60days", + "type": "t_bool", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Token)1475_storage": { + "label": "struct BondingConfig.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)1436_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "afe8cf2df36bb3413beee01fec881aa8adddc5a23ed71706a55033f1e101af07": { + "address": "0x8d2cB249F39f16A58ce5c801427d63A9737B65a3", + "txHash": "0x723757089e3979dd3b263b5c33555f6e478dbaa66f44c4b3c71e45c622c82a54", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "assetToken", + "offset": 0, + "slot": "0", + "type": "t_address", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:50" + }, + { + "label": "taxToken", + "offset": 0, + "slot": "1", + "type": "t_address", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:51" + }, + { + "label": "router", + "offset": 0, + "slot": "2", + "type": "t_contract(IRouter)2539", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:52" + }, + { + "label": "treasury", + "offset": 0, + "slot": "3", + "type": "t_address", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:53" + }, + { + "label": "feeRate", + "offset": 20, + "slot": "3", + "type": "t_uint16", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:54" + }, + { + "label": "minSwapThreshold", + "offset": 0, + "slot": "4", + "type": "t_uint256", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:55" + }, + { + "label": "maxSwapThreshold", + "offset": 0, + "slot": "5", + "type": "t_uint256", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:56" + }, + { + "label": "agentNft", + "offset": 0, + "slot": "6", + "type": "t_contract(IAgentNft)4280", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:57" + }, + { + "label": "_agentTba", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_uint256,t_address)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:74" + }, + { + "label": "taxHistory", + "offset": 0, + "slot": "8", + "type": "t_mapping(t_bytes32,t_struct(TaxHistory)2608_storage)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:75" + }, + { + "label": "agentTaxAmounts", + "offset": 0, + "slot": "9", + "type": "t_mapping(t_uint256,t_struct(TaxAmounts)2613_storage)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:76" + }, + { + "label": "_agentRecipients", + "offset": 0, + "slot": "10", + "type": "t_mapping(t_uint256,t_struct(TaxRecipient)2715_storage)", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:95" + }, + { + "label": "creatorFeeRate", + "offset": 0, + "slot": "11", + "type": "t_uint16", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:96" + }, + { + "label": "tbaBonus", + "offset": 2, + "slot": "11", + "type": "t_contract(ITBABonus)4135", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:104" + }, + { + "label": "bondingV2", + "offset": 0, + "slot": "12", + "type": "t_contract(IBondingV2ForTax)2558", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:105" + }, + { + "label": "bondingV4", + "offset": 0, + "slot": "13", + "type": "t_contract(IBondingV4ForTax)2573", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:106" + }, + { + "label": "bondingV5", + "offset": 0, + "slot": "14", + "type": "t_contract(IBondingV5ForTax)2595", + "contract": "AgentTax", + "src": "contracts/tax/AgentTax.sol:107" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)24_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)34_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)24_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)145_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)24_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_contract(IAgentNft)4280": { + "label": "contract IAgentNft", + "numberOfBytes": "20" + }, + "t_contract(IBondingV2ForTax)2558": { + "label": "contract IBondingV2ForTax", + "numberOfBytes": "20" + }, + "t_contract(IBondingV4ForTax)2573": { + "label": "contract IBondingV4ForTax", + "numberOfBytes": "20" + }, + "t_contract(IBondingV5ForTax)2595": { + "label": "contract IBondingV5ForTax", + "numberOfBytes": "20" + }, + "t_contract(IRouter)2539": { "label": "contract IRouter", "numberOfBytes": "20" }, - "t_contract(ITBABonus)55297": { + "t_contract(ITBABonus)4135": { "label": "contract ITBABonus", "numberOfBytes": "20" }, - "t_mapping(t_bytes32,t_struct(TaxHistory)53360_storage)": { + "t_mapping(t_bytes32,t_struct(TaxHistory)2608_storage)": { "label": "mapping(bytes32 => struct AgentTax.TaxHistory)", "numberOfBytes": "32" }, @@ -2565,15 +3864,15 @@ "label": "mapping(uint256 => address)", "numberOfBytes": "32" }, - "t_mapping(t_uint256,t_struct(TaxAmounts)53365_storage)": { + "t_mapping(t_uint256,t_struct(TaxAmounts)2613_storage)": { "label": "mapping(uint256 => struct AgentTax.TaxAmounts)", "numberOfBytes": "32" }, - "t_mapping(t_uint256,t_struct(TaxRecipient)53467_storage)": { + "t_mapping(t_uint256,t_struct(TaxRecipient)2715_storage)": { "label": "mapping(uint256 => struct AgentTax.TaxRecipient)", "numberOfBytes": "32" }, - "t_struct(TaxAmounts)53365_storage": { + "t_struct(TaxAmounts)2613_storage": { "label": "struct AgentTax.TaxAmounts", "members": [ { @@ -2591,7 +3890,7 @@ ], "numberOfBytes": "64" }, - "t_struct(TaxHistory)53360_storage": { + "t_struct(TaxHistory)2608_storage": { "label": "struct AgentTax.TaxHistory", "members": [ { @@ -2609,7 +3908,7 @@ ], "numberOfBytes": "64" }, - "t_struct(TaxRecipient)53467_storage": { + "t_struct(TaxRecipient)2715_storage": { "label": "struct AgentTax.TaxRecipient", "members": [ { diff --git a/contracts/launchpadv2/BondingConfig.sol b/contracts/launchpadv2/BondingConfig.sol index e72eeeb..af7dd53 100644 --- a/contracts/launchpadv2/BondingConfig.sol +++ b/contracts/launchpadv2/BondingConfig.sol @@ -37,39 +37,6 @@ contract BondingConfig is Initializable, OwnableUpgradeable { // returns individual fields, not the whole struct as memory ScheduledLaunchParams internal _scheduledLaunchParams; - function scheduledLaunchParams() - external - view - returns (ScheduledLaunchParams memory) - { - return _scheduledLaunchParams; - } - - /** - * @notice Calculate total launch fee based on launch type and ACF requirement - * @dev Fee structure: - * - Immediate launch, no ACF: 0 - * - Immediate launch, with ACF: acfFee - * - Scheduled launch, no ACF: normalLaunchFee - * - Scheduled launch, with ACF: normalLaunchFee + acfFee - * @param isScheduledLaunch_ Whether this is a scheduled launch - * @param needAcf_ Whether ACF operations are needed - * @return totalFee The total fee to charge - */ - function calculateLaunchFee( - bool isScheduledLaunch_, - bool needAcf_ - ) external view returns (uint256) { - uint256 totalFee = 0; - if (isScheduledLaunch_) { - totalFee += _scheduledLaunchParams.normalLaunchFee; - } - if (needAcf_) { - totalFee += _scheduledLaunchParams.acfFee; - } - return totalFee; - } - // Global wallet to receive reserved tokens (airdrop + ACF) address public teamTokenReservedWallet; @@ -143,10 +110,6 @@ contract BondingConfig is Initializable, OwnableUpgradeable { // returns individual fields, not the whole struct as memory DeployParams internal _deployParams; - function deployParams() external view returns (DeployParams memory) { - return _deployParams; - } - // Common parameters uint256 public initialSupply; address public feeTo; @@ -359,4 +322,41 @@ contract BondingConfig is Initializable, OwnableUpgradeable { } revert InvalidAntiSniperType(); } + + function scheduledLaunchParams() + external + view + returns (ScheduledLaunchParams memory) + { + return _scheduledLaunchParams; + } + + function deployParams() external view returns (DeployParams memory) { + return _deployParams; + } + + /** + * @notice Calculate total launch fee based on launch type and ACF requirement + * @dev Fee structure: + * - Immediate launch, no ACF: 0 + * - Immediate launch, with ACF: acfFee + * - Scheduled launch, no ACF: normalLaunchFee + * - Scheduled launch, with ACF: normalLaunchFee + acfFee + * @param isScheduledLaunch_ Whether this is a scheduled launch + * @param needAcf_ Whether ACF operations are needed + * @return totalFee The total fee to charge + */ + function calculateLaunchFee( + bool isScheduledLaunch_, + bool needAcf_ + ) external view returns (uint256) { + uint256 totalFee = 0; + if (isScheduledLaunch_) { + totalFee += _scheduledLaunchParams.normalLaunchFee; + } + if (needAcf_) { + totalFee += _scheduledLaunchParams.acfFee; + } + return totalFee; + } } diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 27792bb..99dfa2b 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -134,14 +134,14 @@ contract BondingV5 is } function preLaunch( - string memory _name, - string memory _ticker, - uint8[] memory cores, - string memory desc, - string memory img, - string[4] memory urls, - uint256 purchaseAmount, - uint256 startTime, + string memory name_, + string memory ticker_, + uint8[] memory cores_, + string memory desc_, + string memory img_, + string[4] memory urls_, + uint256 purchaseAmount_, + uint256 startTime_, uint8 launchMode_, uint8 airdropPercent_, bool needAcf_, @@ -157,7 +157,7 @@ contract BondingV5 is revert InvalidAntiSniperType(); } - if (cores.length <= 0) { + if (cores_.length <= 0) { revert InvalidInput(); } @@ -166,7 +166,7 @@ contract BondingV5 is // Scheduled launch: startTime >= now + scheduledLaunchStartTimeDelay BondingConfig.ScheduledLaunchParams memory scheduledParams = bondingConfig.scheduledLaunchParams(); uint256 scheduledThreshold = block.timestamp + scheduledParams.startTimeDelay; - bool isScheduledLaunch = startTime >= scheduledThreshold; + bool isScheduledLaunch = startTime_ >= scheduledThreshold; // Validate launch mode, authorization, and mode-specific requirements _validateLaunchMode( @@ -179,16 +179,16 @@ contract BondingV5 is ); uint256 actualStartTime; - uint256 startTimeDelay; + uint256 actualStartTimeDelay; if (isScheduledLaunch) { // Scheduled launch: use provided startTime - actualStartTime = startTime; - startTimeDelay = scheduledParams.startTimeDelay; + actualStartTime = startTime_; + actualStartTimeDelay = scheduledParams.startTimeDelay; } else { // Immediate launch: start immediately actualStartTime = block.timestamp; - startTimeDelay = 0; + actualStartTimeDelay = 0; } // Calculate launch fee based on launch type and ACF requirement @@ -199,13 +199,13 @@ contract BondingV5 is // - Scheduled launch, with ACF: normalLaunchFee + acfFee uint256 launchFee = bondingConfig.calculateLaunchFee(isScheduledLaunch, needAcf_); - if (purchaseAmount < launchFee) { + if (purchaseAmount_ < launchFee) { revert InvalidInput(); } address assetToken = router.assetToken(); - uint256 initialPurchase = (purchaseAmount - launchFee); + uint256 initialPurchase = (purchaseAmount_ - launchFee); if (launchFee > 0) { IERC20(assetToken).safeTransferFrom(msg.sender, bondingConfig.feeTo(), launchFee); } @@ -215,28 +215,28 @@ contract BondingV5 is initialPurchase ); - uint256 _initialSupply = bondingConfig.initialSupply(); - BondingConfig.DeployParams memory _deployParams = bondingConfig.deployParams(); + uint256 configInitialSupply = bondingConfig.initialSupply(); + BondingConfig.DeployParams memory deployParams = bondingConfig.deployParams(); (address token, uint256 applicationId) = agentFactory .createNewAgentTokenAndApplication( - _name, // without "fun " prefix - _ticker, + name_, // without "fun " prefix + ticker_, abi.encode( // tokenSupplyParams - _initialSupply, + configInitialSupply, 0, // lpSupply, will mint to agentTokenAddress - _initialSupply, // vaultSupply, will mint to vault - _initialSupply, - _initialSupply, + configInitialSupply, // vaultSupply, will mint to vault + configInitialSupply, + configInitialSupply, 0, address(this) // vault, is the bonding contract itself ), - cores, - _deployParams.tbaSalt, - _deployParams.tbaImplementation, - _deployParams.daoVotingPeriod, - _deployParams.daoThreshold, + cores_, + deployParams.tbaSalt, + deployParams.tbaImplementation, + deployParams.daoVotingPeriod, + deployParams.daoThreshold, 0, // applicationThreshold_ msg.sender // token creator ); @@ -249,13 +249,13 @@ contract BondingV5 is // Calculate bonding curve supply in wei (base supply was validated at the beginning) uint256 bondingCurveSupply = bondingCurveSupplyBase * (10 ** IAgentTokenV2(token).decimals()); // Calculate total reserved supply for transfer - uint256 totalReservedSupply = _initialSupply - bondingCurveSupplyBase; + uint256 totalReservedSupply = configInitialSupply - bondingCurveSupplyBase; - address _pair = factory.createPair( + address pair = factory.createPair( token, assetToken, actualStartTime, - startTimeDelay + actualStartTimeDelay ); require(_approval(address(router), token, bondingCurveSupply)); @@ -285,14 +285,14 @@ contract BondingV5 is newToken.creator = msg.sender; newToken.token = token; newToken.agentToken = address(0); - newToken.pair = _pair; - newToken.description = desc; - newToken.cores = cores; - newToken.image = img; - newToken.twitter = urls[0]; - newToken.telegram = urls[1]; - newToken.youtube = urls[2]; - newToken.website = urls[3]; + newToken.pair = pair; + newToken.description = desc_; + newToken.cores = cores_; + newToken.image = img_; + newToken.twitter = urls_[0]; + newToken.telegram = urls_[1]; + newToken.youtube = urls_[2]; + newToken.website = urls_[3]; newToken.trading = true; newToken.tradingOnUniswap = false; newToken.applicationId = applicationId; @@ -311,9 +311,9 @@ contract BondingV5 is // Set Data struct fields newToken.data.token = token; - newToken.data.name = _name; - newToken.data._name = _name; - newToken.data.ticker = _ticker; + newToken.data.name = name_; + newToken.data._name = name_; + newToken.data.ticker = ticker_; newToken.data.supply = bondingCurveSupply; newToken.data.price = price; newToken.data.marketCap = liquidity; @@ -325,61 +325,61 @@ contract BondingV5 is emit PreLaunched( token, - _pair, + pair, tokenInfo[token].virtualId, initialPurchase, tokenLaunchParams[token] ); - return (token, _pair, tokenInfo[token].virtualId, initialPurchase); + return (token, pair, tokenInfo[token].virtualId, initialPurchase); } - function cancelLaunch(address _tokenAddress) public { - BondingConfig.Token storage _token = tokenInfo[_tokenAddress]; + function cancelLaunch(address tokenAddress_) public { + BondingConfig.Token storage tokenRef = tokenInfo[tokenAddress_]; // Validate that the token exists and was properly prelaunched - if (_token.token == address(0) || _token.pair == address(0)) { + if (tokenRef.token == address(0) || tokenRef.pair == address(0)) { revert InvalidInput(); } - if (msg.sender != _token.creator) { + if (msg.sender != tokenRef.creator) { revert InvalidInput(); } // Validate that the token has not been launched (or cancelled) - if (_token.launchExecuted) { + if (tokenRef.launchExecuted) { revert InvalidTokenStatus(); } - if (_token.initialPurchase > 0) { + if (tokenRef.initialPurchase > 0) { IERC20(router.assetToken()).safeTransfer( - _token.creator, - _token.initialPurchase + tokenRef.creator, + tokenRef.initialPurchase ); } - _token.initialPurchase = 0; // prevent duplicate transfer initialPurchase back to the creator - _token.launchExecuted = true; // pretend it has been launched (cancelled) and prevent duplicate launch + tokenRef.initialPurchase = 0; // prevent duplicate transfer initialPurchase back to the creator + tokenRef.launchExecuted = true; // pretend it has been launched (cancelled) and prevent duplicate launch emit CancelledLaunch( - _tokenAddress, - _token.pair, - tokenInfo[_tokenAddress].virtualId, - _token.initialPurchase + tokenAddress_, + tokenRef.pair, + tokenInfo[tokenAddress_].virtualId, + tokenRef.initialPurchase ); } function launch( - address _tokenAddress + address tokenAddress_ ) public nonReentrant returns (address, address, uint, uint256) { - BondingConfig.Token storage _token = tokenInfo[_tokenAddress]; + BondingConfig.Token storage tokenRef = tokenInfo[tokenAddress_]; // Validate that the token exists and was properly prelaunched - if (_token.token == address(0) || _token.pair == address(0)) { + if (tokenRef.token == address(0) || tokenRef.pair == address(0)) { revert InvalidInput(); } - if (_token.launchExecuted) { + if (tokenRef.launchExecuted) { revert InvalidTokenStatus(); } @@ -387,13 +387,13 @@ contract BondingV5 is // while swaps remain blocked (since enabling depends solely on time), // resulting in an inconsistent "launched but not tradable" state, also the 550M is go to teamTokenReservedWallet // so we need to check the start time of the pair - IFPairV2 pair = IFPairV2(_token.pair); - if (block.timestamp < pair.startTime()) { + IFPairV2 pairContract = IFPairV2(tokenRef.pair); + if (block.timestamp < pairContract.startTime()) { revert InvalidInput(); } // Set tax start time to current block timestamp for proper anti-sniper tax calculation - router.setTaxStartTime(_token.pair, block.timestamp); + router.setTaxStartTime(tokenRef.pair, block.timestamp); // Make initial purchase for creator // bondingContract will transfer initialPurchase $Virtual to pairAddress @@ -401,7 +401,7 @@ contract BondingV5 is // bondingContract then will transfer all the amountsOut $agentToken to teamTokenReservedWallet // in the BE, teamTokenReservedWallet will split it out for the initialBuy and 550M uint256 amountOut = 0; - uint256 initialPurchase = _token.initialPurchase; + uint256 initialPurchase = tokenRef.initialPurchase; if (initialPurchase > 0) { IERC20(router.assetToken()).forceApprove( address(router), @@ -410,172 +410,172 @@ contract BondingV5 is amountOut = _buy( address(this), initialPurchase, // will raise error if initialPurchase <= 0 - _tokenAddress, + tokenAddress_, 0, block.timestamp + 300, true // isInitialPurchase = true for creator's purchase ); // creator's initialBoughtToken need to go to teamTokenReservedWallet for locking, not to creator - IERC20(_tokenAddress).safeTransfer( + IERC20(tokenAddress_).safeTransfer( bondingConfig.teamTokenReservedWallet(), amountOut ); // update initialPurchase and launchExecuted to prevent duplicate purchase - _token.initialPurchase = 0; + tokenRef.initialPurchase = 0; } emit Launched( - _tokenAddress, - _token.pair, - tokenInfo[_tokenAddress].virtualId, + tokenAddress_, + tokenRef.pair, + tokenInfo[tokenAddress_].virtualId, initialPurchase, amountOut, - tokenLaunchParams[_tokenAddress] + tokenLaunchParams[tokenAddress_] ); - _token.launchExecuted = true; + tokenRef.launchExecuted = true; return ( - _tokenAddress, - _token.pair, - tokenInfo[_tokenAddress].virtualId, + tokenAddress_, + tokenRef.pair, + tokenInfo[tokenAddress_].virtualId, initialPurchase ); } function sell( - uint256 amountIn, - address tokenAddress, - uint256 amountOutMin, - uint256 deadline + uint256 amountIn_, + address tokenAddress_, + uint256 amountOutMin_, + uint256 deadline_ ) public returns (bool) { // this alrealy prevented it's a not-exists token - if (!tokenInfo[tokenAddress].trading) { + if (!tokenInfo[tokenAddress_].trading) { revert InvalidTokenStatus(); } // this is to prevent sell before launch - if (!tokenInfo[tokenAddress].launchExecuted) { + if (!tokenInfo[tokenAddress_].launchExecuted) { revert InvalidTokenStatus(); } - if (block.timestamp > deadline) { + if (block.timestamp > deadline_) { revert InvalidInput(); } (uint256 amount0In, uint256 amount1Out) = router.sell( - amountIn, - tokenAddress, + amountIn_, + tokenAddress_, msg.sender ); - if (amount1Out < amountOutMin) { + if (amount1Out < amountOutMin_) { revert SlippageTooHigh(); } uint256 duration = block.timestamp - - tokenInfo[tokenAddress].data.lastUpdated; + tokenInfo[tokenAddress_].data.lastUpdated; if (duration > 86400) { - tokenInfo[tokenAddress].data.lastUpdated = block.timestamp; + tokenInfo[tokenAddress_].data.lastUpdated = block.timestamp; } return true; } function _buy( - address buyer, - uint256 amountIn, - address tokenAddress, - uint256 amountOutMin, - uint256 deadline, - bool isInitialPurchase + address buyer_, + uint256 amountIn_, + address tokenAddress_, + uint256 amountOutMin_, + uint256 deadline_, + bool isInitialPurchase_ ) internal returns (uint256) { - if (block.timestamp > deadline) { + if (block.timestamp > deadline_) { revert InvalidInput(); } address pairAddress = factory.getPair( - tokenAddress, + tokenAddress_, router.assetToken() ); - IFPairV2 pair = IFPairV2(pairAddress); + IFPairV2 pairContract = IFPairV2(pairAddress); - (uint256 reserveA, uint256 reserveB) = pair.getReserves(); + (uint256 reserveA, uint256 reserveB) = pairContract.getReserves(); (uint256 amount1In, uint256 amount0Out) = router.buy( - amountIn, - tokenAddress, - buyer, - isInitialPurchase + amountIn_, + tokenAddress_, + buyer_, + isInitialPurchase_ ); - if (amount0Out < amountOutMin) { + if (amount0Out < amountOutMin_) { revert SlippageTooHigh(); } uint256 newReserveA = reserveA - amount0Out; uint256 duration = block.timestamp - - tokenInfo[tokenAddress].data.lastUpdated; + tokenInfo[tokenAddress_].data.lastUpdated; if (duration > 86400) { - tokenInfo[tokenAddress].data.lastUpdated = block.timestamp; + tokenInfo[tokenAddress_].data.lastUpdated = block.timestamp; } // Get per-token gradThreshold (calculated during preLaunch based on airdropPercent and needAcf) - uint256 gradThreshold = tokenGradThreshold[tokenAddress]; + uint256 gradThreshold = tokenGradThreshold[tokenAddress_]; if ( newReserveA <= gradThreshold && !router.hasAntiSniperTax(pairAddress) && - tokenInfo[tokenAddress].trading + tokenInfo[tokenAddress_].trading ) { - _openTradingOnUniswap(tokenAddress); + _openTradingOnUniswap(tokenAddress_); } return amount0Out; } function buy( - uint256 amountIn, - address tokenAddress, - uint256 amountOutMin, - uint256 deadline + uint256 amountIn_, + address tokenAddress_, + uint256 amountOutMin_, + uint256 deadline_ ) public payable returns (bool) { // this alrealy prevented it's a not-exists token - if (!tokenInfo[tokenAddress].trading) { + if (!tokenInfo[tokenAddress_].trading) { revert InvalidTokenStatus(); } // this is to prevent sell before launch - if (!tokenInfo[tokenAddress].launchExecuted) { + if (!tokenInfo[tokenAddress_].launchExecuted) { revert InvalidTokenStatus(); } - _buy(msg.sender, amountIn, tokenAddress, amountOutMin, deadline, false); + _buy(msg.sender, amountIn_, tokenAddress_, amountOutMin_, deadline_, false); return true; } - function _openTradingOnUniswap(address tokenAddress) private { - BondingConfig.Token storage _token = tokenInfo[tokenAddress]; + function _openTradingOnUniswap(address tokenAddress_) private { + BondingConfig.Token storage tokenRef = tokenInfo[tokenAddress_]; - if (_token.tradingOnUniswap || !_token.trading) { + if (tokenRef.tradingOnUniswap || !tokenRef.trading) { revert InvalidTokenStatus(); } // Transfer asset tokens to bonding contract address pairAddress = factory.getPair( - tokenAddress, + tokenAddress_, router.assetToken() ); - IFPairV2 pair = IFPairV2(pairAddress); + IFPairV2 pairContract = IFPairV2(pairAddress); - uint256 assetBalance = pair.assetBalance(); - uint256 tokenBalance = pair.balance(); + uint256 assetBalance = pairContract.assetBalance(); + uint256 tokenBalance = pairContract.balance(); - router.graduate(tokenAddress); + router.graduate(tokenAddress_); // previously initFromBondingCurve has two parts: // 1. transfer applicationThreshold_ assetToken from bondingContract to agentFactoryV3Contract @@ -587,33 +587,33 @@ contract BondingV5 is ); agentFactory .updateApplicationThresholdWithApplicationId( - _token.applicationId, + tokenRef.applicationId, assetBalance ); // remove blacklist address after graduation, cuz executeBondingCurveApplicationSalt we will transfer all left agentTokens to the uniswapV2Pair agentFactory.removeBlacklistAddress( - tokenAddress, - IAgentTokenV2(tokenAddress).liquidityPools()[0] + tokenAddress_, + IAgentTokenV2(tokenAddress_).liquidityPools()[0] ); // previously executeBondingCurveApplicationSalt will create agentToken and do two parts: // 1. (lpSupply = all left $preToken in prePairAddress) $agentToken mint to agentTokenAddress // 2. (vaultSupply = 1B - lpSupply) $agentToken mint to prePairAddress // now only need to transfer (all left agentTokens) $agentTokens from agentFactoryV6Address to agentTokenAddress - IERC20(tokenAddress).safeTransfer(tokenAddress, tokenBalance); - require(_token.applicationId != 0, "ApplicationId not found"); + IERC20(tokenAddress_).safeTransfer(tokenAddress_, tokenBalance); + require(tokenRef.applicationId != 0, "ApplicationId not found"); address agentToken = agentFactory .executeBondingCurveApplicationSalt( - _token.applicationId, - _token.data.supply / 1 ether, // totalSupply + tokenRef.applicationId, + tokenRef.data.supply / 1 ether, // totalSupply tokenBalance / 1 ether, // lpSupply pairAddress, // vault keccak256( - abi.encodePacked(msg.sender, block.timestamp, tokenAddress) + abi.encodePacked(msg.sender, block.timestamp, tokenAddress_) ) ); - _token.agentToken = agentToken; + tokenRef.agentToken = agentToken; // this is not needed, previously need to do this because of // 1. (vaultSupply = 1B - lpSupply) $agentToken will mint to prePairAddress @@ -625,39 +625,39 @@ contract BondingV5 is // IERC20(agentToken).balanceOf(pairAddress) // ); - emit Graduated(tokenAddress, agentToken); - _token.trading = false; - _token.tradingOnUniswap = true; + emit Graduated(tokenAddress_, agentToken); + tokenRef.trading = false; + tokenRef.tradingOnUniswap = true; } // View functions to check token launch type (affects tax recipient updates and liquidity drain permissions) - function isProject60days(address token) external view returns (bool) { - return tokenLaunchParams[token].isProject60days; + function isProject60days(address token_) external view returns (bool) { + return tokenLaunchParams[token_].isProject60days; } - function isProjectXLaunch(address token) external view returns (bool) { - return tokenLaunchParams[token].launchMode == bondingConfig.LAUNCH_MODE_X_LAUNCH(); + function isProjectXLaunch(address token_) external view returns (bool) { + return tokenLaunchParams[token_].launchMode == bondingConfig.LAUNCH_MODE_X_LAUNCH(); } - function isAcpSkillLaunch(address token) external view returns (bool) { - return tokenLaunchParams[token].launchMode == bondingConfig.LAUNCH_MODE_ACP_SKILL(); + function isAcpSkillLaunch(address token_) external view returns (bool) { + return tokenLaunchParams[token_].launchMode == bondingConfig.LAUNCH_MODE_ACP_SKILL(); } // View function for FRouterV2 to get anti-sniper tax type // Reverts if token was not created by BondingV5, allowing FRouterV2 to fallback to legacy logic - function tokenAntiSniperType(address token) external view returns (uint8) { - if (tokenInfo[token].creator == address(0)) { + function tokenAntiSniperType(address token_) external view returns (uint8) { + if (tokenInfo[token_].creator == address(0)) { revert InvalidTokenStatus(); } - return tokenLaunchParams[token].antiSniperTaxType; + return tokenLaunchParams[token_].antiSniperTaxType; } function _approval( - address _spender, - address _token, - uint256 amount + address spender_, + address token_, + uint256 amount_ ) internal returns (bool) { - IERC20(_token).forceApprove(_spender, amount); + IERC20(token_).forceApprove(spender_, amount_); return true; } diff --git a/contracts/tax/AgentTax.sol b/contracts/tax/AgentTax.sol index 6ae3c42..2788b58 100644 --- a/contracts/tax/AgentTax.sol +++ b/contracts/tax/AgentTax.sol @@ -21,6 +21,13 @@ interface IBondingV4ForTax { function isAcpSkillLaunch(address token) external view returns (bool); } +// Minimal interface for BondingV5 (combines V2 and V4 functionality) +interface IBondingV5ForTax { + function isProject60days(address token) external view returns (bool); + function isProjectXLaunch(address token) external view returns (bool); + function isAcpSkillLaunch(address token) external view returns (bool); +} + contract AgentTax is Initializable, AccessControlUpgradeable { using SafeERC20 for IERC20; struct TaxHistory { @@ -86,7 +93,7 @@ contract AgentTax is Initializable, AccessControlUpgradeable { ); mapping(uint256 agentId => TaxRecipient) private _agentRecipients; - uint16 public creatorFeeRate; + uint16 public creatorFeeRate; // actually this variable is not used event CreatorUpdated( uint256 agentId, @@ -94,9 +101,10 @@ contract AgentTax is Initializable, AccessControlUpgradeable { address newCreator ); - ITBABonus public tbaBonus; + ITBABonus public tbaBonus; // deprecated IBondingV2ForTax public bondingV2; IBondingV4ForTax public bondingV4; + IBondingV5ForTax public bondingV5; /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -131,8 +139,8 @@ contract AgentTax is Initializable, AccessControlUpgradeable { IERC20(taxToken).forceApprove(router_, type(uint256).max); agentNft = IAgentNft(nft_); - feeRate = 100; - creatorFeeRate = 3000; + feeRate = 3000; // set init value, so no need to call updateSwapParams after deployment + creatorFeeRate = 7000; // set init value, so no need to call updateSwapParams after deployment emit SwapParamsUpdated2( address(0), @@ -296,13 +304,6 @@ contract AgentTax is Initializable, AccessControlUpgradeable { taxRecipient.creator, creatorFee ); - if (address(tbaBonus) != address(0)) { - tbaBonus.distributeBonus( - agentId, - taxRecipient.creator, - creatorFee - ); - } } if (feeAmount > 0) { @@ -373,7 +374,7 @@ contract AgentTax is Initializable, AccessControlUpgradeable { } /** - * @notice Update tax recipient for new feature agents launched via BondingV2 + * @notice Update tax recipient for new feature agents launched via BondingV2 or BondingV5 * @param agentId The agentId (virtualId) of the agent * @param tba The Token Bound Address for the agent * @param creator The creator address that will receive tax rewards @@ -383,18 +384,21 @@ contract AgentTax is Initializable, AccessControlUpgradeable { address tba, address creator ) public onlyRole(EXECUTOR_V2_ROLE) { - require(address(bondingV2) != address(0), "BondingV2 not set"); - // Get token address from agentId IAgentNft.VirtualInfo memory info = agentNft.virtualInfo(agentId); address token = info.token; require(token != address(0), "Token not found"); - // Check if this is a Project60days token - require( - bondingV2.isProject60days(token), - "Token is not a Project60days token" - ); + // Check if this is a Project60days token from any of BondingV2, BondingV4, or BondingV5 + // Once true, stays true (using OR logic to prevent overwriting) + bool isProject60days = false; + if (address(bondingV5) != address(0)) { + isProject60days = isProject60days || bondingV5.isProject60days(token); + } + if (address(bondingV2) != address(0)) { + isProject60days = isProject60days || bondingV2.isProject60days(token); + } + require(isProject60days, "Token is not a Project60days token"); require(tba != address(0), "Invalid TBA"); require(creator != address(0), "Invalid creator"); @@ -416,7 +420,16 @@ contract AgentTax is Initializable, AccessControlUpgradeable { } /** - * @notice Update tax recipient for special launch mode agents (X_LAUNCH or ACP_SKILL) via BondingV4 + * @notice Set BondingV5 contract address (combines V2 and V4 functionality) + * @param bondingV5_ The address of the BondingV5 contract + */ + function setBondingV5(address bondingV5_) public onlyRole(ADMIN_ROLE) { + require(bondingV5_ != address(0), "Invalid BondingV5 address"); + bondingV5 = IBondingV5ForTax(bondingV5_); + } + + /** + * @notice Update tax recipient for special launch mode agents (X_LAUNCH or ACP_SKILL) via BondingV4 or BondingV5 * @param agentId The agentId (virtualId) of the agent * @param tba The Token Bound Address for the agent * @param creator The creator address that will receive tax rewards @@ -426,19 +439,21 @@ contract AgentTax is Initializable, AccessControlUpgradeable { address tba, address creator ) public onlyRole(EXECUTOR_V2_ROLE) { - require(address(bondingV4) != address(0), "BondingV4 not set"); - // Get token address from agentId IAgentNft.VirtualInfo memory info = agentNft.virtualInfo(agentId); address token = info.token; require(token != address(0), "Token not found"); - // Check if this is a special launch mode token (X_LAUNCH or ACP_SKILL) - require( - bondingV4.isProjectXLaunch(token) || - bondingV4.isAcpSkillLaunch(token), - "Token is not X_LAUNCH or ACP_SKILL" - ); + // Check if this is a special launch mode token (X_LAUNCH or ACP_SKILL) from any of BondingV4 or BondingV5 + // Once true, stays true (using OR logic to prevent overwriting) + bool isSpecialMode = false; + if (address(bondingV5) != address(0)) { + isSpecialMode = isSpecialMode || bondingV5.isProjectXLaunch(token) || bondingV5.isAcpSkillLaunch(token); + } + if (address(bondingV4) != address(0)) { + isSpecialMode = isSpecialMode || bondingV4.isProjectXLaunch(token) || bondingV4.isAcpSkillLaunch(token); + } + require(isSpecialMode, "Token is not X_LAUNCH or ACP_SKILL"); require(tba != address(0), "Invalid TBA"); require(creator != address(0), "Invalid creator"); @@ -478,8 +493,4 @@ contract AgentTax is Initializable, AccessControlUpgradeable { _swapForAsset(agentId, minOutput, maxOverride); } } - - function updateTbaBonus(address tbaBonus_) public onlyRole(ADMIN_ROLE) { - tbaBonus = ITBABonus(tbaBonus_); - } } diff --git a/hardhat.config.js b/hardhat.config.js index d055972..0f9f611 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -78,12 +78,16 @@ module.exports = { ], }, networks: { + eth_mainnet: { + url: process.env.ETH_MAINNET_RPC_URL || "https://eth.drpc.org", + accounts: [process.env.PRIVATE_KEY], + }, sepolia: { - url: "https://sepolia.drpc.org", + url: process.env.SEPOLIA_RPC_URL || "https://sepolia.drpc.org", accounts: [process.env.PRIVATE_KEY], }, eth_sepolia: { - url: "https://sepolia.drpc.org", + url: process.env.ETH_SEPOLIA_RPC_URL || "https://sepolia.drpc.org", accounts: [process.env.PRIVATE_KEY], }, base: { diff --git a/scripts/launchpadv5/AUDIT_SUMMARY.md b/scripts/launchpadv5/AUDIT_SUMMARY.md new file mode 100644 index 0000000..70e5ce9 --- /dev/null +++ b/scripts/launchpadv5/AUDIT_SUMMARY.md @@ -0,0 +1,450 @@ +# Smart Contract Audit Summary - LaunchpadV5 + +**Branch:** `feat/vp-2016` +**Commit:** `latest commit from git branch feat/vp-2016` +**Date:** March 2026 + +--- + +## 1. Context & Motivation + +### Background + +Virtuals Protocol currently operates bonding curve launches across multiple chains (Base, Ethereum, Solana). The existing implementation (`BondingV2`), (`BondingV3 (deprecated)`), (`BondingV4`) has accumulated various launch modes and configurations that were added incrementally: + +- **Normal Launch**: Standard bonding curve with 99-minute anti-sniper tax decay +- **X-Launch**: Partnership launches with 99-second anti-sniper tax decay +- **ACP Skill Launch**: Agent Commerce Protocol skill-based launches +- **Project 60-days**: Time-locked creator reward distribution +- **ACF (Agent Creation Fund)**: 50% token reserve for operations + +Each mode was implemented separately, leading to: +1. Fragmented logic across multiple contracts +2. Hardcoded parameters that require contract upgrades to modify +3. Difficulty in deploying consistent configurations across new chains + +### Goal + +**Unify the launch mechanism into a more flexible and configurable architecture** to: +1. Enable easier multi-chain expansion with chain-specific configurations +2. Consolidate all launch modes into a single, parameterized system +3. Make bonding curve parameters adjustable without contract upgrades +4. Improve code maintainability and auditability + +--- + +## 2. Scope of Changes + +### New Contracts + +| Contract | Lines | Description | +|----------|-------|-------------| +| `BondingConfig.sol` | 362 | Configuration contract storing all adjustable parameters | +| `BondingV5.sol` | 718 | New bonding curve contract with unified launch logic | + +### Modified Contracts + +| Contract | Changes | Description | +|----------|---------|-------------| +| `FRouterV2.sol` | +129 lines | Added BondingV5 integration for configurable anti-sniper tax | + +--- + +## 3. Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ User (Creator) │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ preLaunch() / launch() / buy() / sell() + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ BondingV5 │ +│ - Unified launch logic for all modes │ +│ - Per-token configurable parameters │ +│ - Graduation threshold calculated per-token │ +└─────────────────────────────────────────────────────────────────┘ + │ │ │ + │ reads config │ creates pair │ creates token + ▼ ▼ ▼ +┌───────────────┐ ┌───────────────┐ ┌───────────────────┐ +│ BondingConfig │ │ FFactoryV2 │ │ AgentFactoryV6 │ +│ │ │ │ │ │ +│ - Launch fees │ │ - Creates │ │ - Creates agent │ +│ - Bonding │ │ FPairV2 │ │ token │ +│ curve params│ │ │ │ - Manages │ +│ - Anti-sniper │ └───────────────┘ │ applications │ +│ types │ │ └───────────────────┘ +│ - Authorized │ │ +│ launchers │ ▼ +└───────────────┘ ┌───────────────┐ + │ FRouterV2 │ + │ │ + │ - Executes │ + │ trades │ + │ - Calculates │ + │ anti-sniper │ + │ tax │ + └───────────────┘ +``` + +--- + +## 4. Key Changes Detail + +### 4.1 BondingConfig.sol (New) + +**Purpose:** Centralized configuration contract for multi-chain deployments. + +#### Launch Mode Constants +```solidity +uint8 public constant LAUNCH_MODE_NORMAL = 0; // Open to everyone +uint8 public constant LAUNCH_MODE_X_LAUNCH = 1; // Requires isXLauncher authorization +uint8 public constant LAUNCH_MODE_ACP_SKILL = 2; // Requires isAcpSkillLauncher authorization +``` + +#### Anti-Sniper Tax Type Constants +```solidity +uint8 public constant ANTI_SNIPER_NONE = 0; // No anti-sniper tax (0 seconds) +uint8 public constant ANTI_SNIPER_60S = 1; // 60 seconds duration +uint8 public constant ANTI_SNIPER_98M = 2; // 98 minutes duration (5880 seconds) +``` + +#### Reserve Percentage Constants +```solidity +uint8 public constant MAX_TOTAL_RESERVED_PERCENT = 55; // At least 45% must remain in bonding curve +uint8 public constant ACF_RESERVED_PERCENT = 50; // ACF operations reserve 50% +``` + +#### Configurable Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `initialSupply` | `uint256` | Token initial supply (e.g., 1 billion) | +| `feeTo` | `address` | Address to receive launch fees | +| `teamTokenReservedWallet` | `address` | Wallet for airdrop + ACF reserved tokens | +| `maxAirdropPercent` | `uint8` | Maximum allowed airdrop percentage (e.g., 5%) | +| `scheduledLaunchParams` | `struct` | startTimeDelay, normalLaunchFee, acfFee | +| `deployParams` | `struct` | tbaSalt, tbaImplementation, daoVotingPeriod, daoThreshold | +| `bondingCurveParams` | `struct` | fakeInitialVirtualLiq, targetRealVirtual | +| `isXLauncher` | `mapping` | Authorized X-Launch addresses | +| `isAcpSkillLauncher` | `mapping` | Authorized ACP Skill Launch addresses | + +#### Key Functions + +```solidity +// Calculate bonding curve supply after reserves +function calculateBondingCurveSupply(uint8 airdropPercent_, bool needAcf_) + external view returns (uint256); + +// Calculate graduation threshold for a token +function calculateGradThreshold(uint256 bondingCurveSupplyWei_) + external view returns (uint256); + +// Calculate launch fee based on launch type +function calculateLaunchFee(bool isScheduledLaunch_, bool needAcf_) + external view returns (uint256); + +// Get anti-sniper duration for a given type +function getAntiSniperDuration(uint8 antiSniperType_) + external pure returns (uint256); +``` + +--- + +### 4.2 BondingV5.sol (New) + +**Purpose:** Unified bonding curve contract with per-token configurable parameters. + +#### Per-Token Storage + +```solidity +// Stores token info (same as BondingV4) +mapping(address => BondingConfig.Token) public tokenInfo; + +// NEW: Stores configurable launch parameters per token +mapping(address => BondingConfig.LaunchParams) public tokenLaunchParams; + +// NEW: Stores graduation threshold per token (calculated based on airdropPercent and needAcf) +mapping(address => uint256) public tokenGradThreshold; +``` + +#### LaunchParams Structure +```solidity +struct LaunchParams { + uint8 launchMode; // 0=Normal, 1=X-Launch, 2=ACP-Skill + uint8 airdropPercent; // 0-5% + bool needAcf; // Whether 50% is reserved for ACF + uint8 antiSniperTaxType; // 0=None, 1=60s, 2=98min + bool isProject60days; // Whether creator rewards are time-locked +} +``` + +#### preLaunch Function + +**New Signature:** +```solidity +function preLaunch( + string memory name_, + string memory ticker_, + uint8[] memory cores_, + string memory desc_, + string memory img_, + string[4] memory urls_, + uint256 purchaseAmount_, + uint256 startTime_, + // NEW: Configurable parameters + uint8 launchMode_, + uint8 airdropPercent_, + bool needAcf_, + uint8 antiSniperTaxType_, + bool isProject60days_ +) public nonReentrant returns (address, address, uint, uint256) +``` + +**Key Logic Changes:** + +1. **Fail-Fast Validation:** + - Validates `airdropPercent <= maxAirdropPercent` + - Validates `totalReserved < MAX_TOTAL_RESERVED_PERCENT` (55%) + - Validates `antiSniperTaxType` is valid (0, 1, or 2) + +2. **Launch Type Determination:** + - Immediate launch: `startTime < now + scheduledLaunchParams.startTimeDelay` + - Scheduled launch: `startTime >= now + scheduledLaunchParams.startTimeDelay` + +3. **Fee Calculation:** + - Immediate + no ACF: `0` + - Immediate + ACF: `acfFee` + - Scheduled + no ACF: `normalLaunchFee` + - Scheduled + ACF: `normalLaunchFee + acfFee` + +4. **Special Mode Validation:** + - X-Launch and ACP-Skill modes require: + - `antiSniperTaxType = ANTI_SNIPER_NONE` + - Immediate launch (not scheduled) + - `airdropPercent = 0` + - `needAcf = false` + - `isProject60days = false` + +5. **Per-Token Graduation Threshold:** + - Calculated during `preLaunch` based on token's specific `airdropPercent` and `needAcf` + - Formula: `gradThreshold = y0 * x0 / (targetRealVirtual + y0)` + - Where: `y0 = fakeInitialVirtualLiq`, `x0 = bondingCurveSupply` + +#### View Functions for External Contracts + +```solidity +// Used by AgentTax for tax recipient updates +function isProject60days(address token_) external view returns (bool); +function isProjectXLaunch(address token_) external view returns (bool); +function isAcpSkillLaunch(address token_) external view returns (bool); + +// Used by FRouterV2 for anti-sniper tax calculation +function tokenAntiSniperType(address token_) external view returns (uint8); +``` + +--- + +### 4.3 FRouterV2.sol (Modified) + +**Purpose:** Support BondingV5's configurable anti-sniper tax types. + +#### New State Variables +```solidity +IBondingV5ForRouter public bondingV5; +IBondingConfigForRouter public bondingConfig; +``` + +#### New Setter Function +```solidity +function setBondingV5(address bondingV5_, address bondingConfig_) + public onlyRole(ADMIN_ROLE); +``` + +#### Modified Anti-Sniper Tax Calculation + +**Previous Logic (BondingV4):** +- Regular tokens: 99% → 0% over 99 minutes (1% per minute) +- X-Launch tokens: 99% → 0% over 99 seconds (1% per second) + +**New Logic (BondingV5):** +1. Check if token exists in BondingV5 +2. Get `antiSniperTaxType` from BondingV5 +3. Get duration from BondingConfig: + - `ANTI_SNIPER_NONE (0)`: 0 seconds → immediate 0% tax + - `ANTI_SNIPER_60S (1)`: 60 seconds → linear decay from 99% to 0% + - `ANTI_SNIPER_98M (2)`: 5880 seconds → linear decay from 99% to 0% +4. Fall back to legacy logic if token not in BondingV5 + +**Tax Formula:** +``` +tax = startTax * (duration - timeElapsed) / duration +``` + +--- + +## 5. Security Considerations + +### 5.1 Access Control + +| Contract | Role | Permissions | +|----------|------|-------------| +| BondingConfig | `owner` | All setter functions | +| BondingV5 | `owner` | `setBondingConfig()` | +| FRouterV2 | `ADMIN_ROLE` | `setBondingV5()` | + +### 5.2 Potential Attack Vectors to Review + +1. **Reserve Percentage Manipulation** + - Ensure `MAX_TOTAL_RESERVED_PERCENT` (55%) cannot be bypassed + - Verify `airdropPercent + ACF_RESERVED_PERCENT` is properly validated + +2. **Launch Mode Authorization** + - Verify `isXLauncher` and `isAcpSkillLauncher` mappings are only modifiable by owner + - Ensure special modes enforce all parameter restrictions + +3. **Graduation Threshold Calculation** + - Verify per-token `tokenGradThreshold` cannot be manipulated post-launch + - Ensure calculation uses correct bonding curve supply + +4. **Anti-Sniper Tax Bypass** + - Verify `FRouterV2` correctly falls back to legacy logic for non-BondingV5 tokens + - Ensure `tokenAntiSniperType` reverts for non-existent tokens + +5. **Fee Collection** + - Verify launch fees are transferred to `feeTo` before token creation + - Ensure `purchaseAmount >= launchFee` check is enforced + +6. **Reentrancy** + - `BondingV5.preLaunch()` uses `nonReentrant` modifier + - Verify all external calls follow checks-effects-interactions pattern + +### 5.3 Backward Compatibility + +- `FRouterV2` maintains full backward compatibility with BondingV4 and older tokens +- Anti-sniper tax calculation falls back to legacy logic if `bondingV5` is not set or token not found + +--- + +## 6. Test Coverage + +Test file: `test/launchpadv5/bondingV5.js` (2274 lines) + +Key test scenarios: +- Normal launch with various parameter combinations +- X-Launch and ACP-Skill authorization checks +- Scheduled vs immediate launch fee calculation +- Anti-sniper tax type validation and decay +- Graduation threshold calculation with different airdrop/ACF settings +- Reserve percentage boundary tests + +--- + +## 7. Deployment Scripts + +| Script | Purpose | +|--------|---------| +| `deployLaunchpadv5_0.ts` | Deploy AgentNftV2, AgentTax | +| `deployLaunchpadv5_1.ts` | Deploy FFactoryV2, FRouterV2 | +| `deployLaunchpadv5_2.ts` | Deploy AgentFactoryV6 | +| `deployLaunchpadv5_3.ts` | Deploy BondingConfig, BondingV5 | +| `deployLaunchpadv5_4.ts` | Revoke deployer roles | + +--- + +## 8. Testing & Utility Scripts + +### 8.1 e2e_test.ts + +**Purpose:** Comprehensive end-to-end test script for BondingV5 on testnets (e.g., Base Sepolia, ETH Sepolia). + +**Features:** +- Verifies all configuration parameters from BondingConfig +- Checks role assignments (BONDING_ROLE, CREATOR_ROLE, EXECUTOR_ROLE) +- Tests complete token lifecycle: + 1. `preLaunch()` with configurable parameters (launch mode, airdrop, ACF, anti-sniper type) + 2. `launch()` to start trading + 3. `buy()` and `sell()` operations on bonding curve + 4. Graduation trigger when threshold is reached +- Validates fee collection and distribution +- Tests anti-sniper tax decay over time + +**Usage:** +```bash +npx hardhat run scripts/launchpadv5/e2e_test.ts --network base_sepolia +``` + +### 8.2 deploy_agent_tax_swap.ts + +**Purpose:** Deploy a mock tax token and set up Uniswap V2 liquidity pool for testing AgentTax swap functionality. + +**Features:** +1. Deploys MockERC20 as a test "tax token" (e.g., fake WETH) +2. Creates VIRTUAL/TAX liquidity pool on Uniswap V2 +3. Tests bidirectional swaps (VIRTUAL -> TAX and TAX -> VIRTUAL) +4. Verifies AMM functionality for AgentTax's `handleAgentTaxes()` swap logic + +**Use Case:** Required for testing AgentTax on chains where the tax token (e.g., WETH) doesn't have sufficient VIRTUAL liquidity. + +**Usage:** +```bash +npx hardhat run scripts/launchpadv5/deploy_agent_tax_swap.ts --network eth_sepolia +``` + +### 8.3 handle_agent_tax.ts + +**Purpose:** Manual script to call `AgentTax.handleAgentTaxes()` for distributing collected tax to creator and treasury. + +**Features:** +- Calls `handleAgentTaxes(agentId, txhashes, amounts, minOutput)` on AgentTax contract +- Validates EXECUTOR_ROLE permission before execution +- Checks if txhashes have already been processed +- Shows before/after tax amounts for verification +- Includes 5-second confirmation delay for safety + +**Key Parameters:** +- `AGENT_ID`: NFT token ID from AgentNft (not BondingV5's virtualId) +- `TX_HASHES`: Array of transaction hashes that collected tax (bytes32) +- `AMOUNTS`: Corresponding tax amounts in wei +- `MIN_OUTPUT`: Slippage protection for swap + +**Usage:** +```bash +npx hardhat run scripts/launchpadv5/handle_agent_tax.ts --network +``` + +--- + +## 9. Files Changed Summary + +``` +contracts/launchpadv2/BondingConfig.sol | 362 +++++ (NEW) +contracts/launchpadv2/BondingV5.sol | 718 +++++ (NEW) +contracts/launchpadv2/FRouterV2.sol | 129 +- (MODIFIED) +test/launchpadv5/bondingV5.js | 2274 +++++ (NEW) +scripts/launchpadv5/*.ts | ~2000 lines (NEW) +``` + +--- + +## 10. Audit Checklist + +- [ ] BondingConfig parameter validation and access control +- [ ] BondingV5 launch mode authorization logic +- [ ] Reserve percentage calculations (airdrop + ACF) +- [ ] Graduation threshold calculation per token +- [ ] Fee collection and transfer logic +- [ ] Anti-sniper tax calculation in FRouterV2 +- [ ] Backward compatibility with BondingV4 tokens +- [ ] Reentrancy protection +- [ ] Integer overflow/underflow (Solidity 0.8.20) +- [ ] External call safety +- [ ] Event emission correctness + +--- + +## 11. Contact + +For questions about this audit scope, please contact the Virtuals Protocol team. diff --git a/scripts/launchpadv5/deployLaunchpadv5_0.ts b/scripts/launchpadv5/deployLaunchpadv5_0.ts index 9741a60..321f5b5 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_0.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_0.ts @@ -1,5 +1,6 @@ // npx hardhat run scripts/launchpadv5/deployLaunchpadv5_0.ts --network eth_sepolia import { parseEther } from "ethers"; +import { verifyContract } from "./utils"; const { ethers, upgrades } = require("hardhat"); /** @@ -31,6 +32,26 @@ const { ethers, upgrades } = require("hardhat"); throw new Error("ADMIN not set in environment"); } + // Backend wallet addresses for EXECUTOR_V2_ROLE (comma-separated) + const beTaxOpsWallets = process.env.BE_TAX_OPS_WALLETS; + if (!beTaxOpsWallets) { + throw new Error("BE_TAX_OPS_WALLETS not set in environment (comma-separated addresses)"); + } + const beTaxOpsWalletList = beTaxOpsWallets.split(",").map((addr) => addr.trim()).filter((addr) => addr.length > 0); + if (beTaxOpsWalletList.length === 0) { + throw new Error("BE_TAX_OPS_WALLETS must contain at least one address"); + } + + // Backend wallet addresses for EXECUTOR_ROLE (comma-separated), calling handleAgentTaxes() + const beHandleAgentTaxesWallets = process.env.BE_HANDLE_AGENT_TAXES_WALLETS; + if (!beHandleAgentTaxesWallets) { + throw new Error("BE_HANDLE_AGENT_TAXES_WALLETS not set in environment (comma-separated addresses)"); + } + const beHandleAgentTaxesWalletList = beHandleAgentTaxesWallets.split(",").map((addr) => addr.trim()).filter((addr) => addr.length > 0); + if (beHandleAgentTaxesWalletList.length === 0) { + throw new Error("BE_HANDLE_AGENT_TAXES_WALLETS must contain at least one address"); + } + // AgentTax parameters const assetToken = process.env.AGENT_TAX_ASSET_TOKEN; if (!assetToken) { @@ -68,6 +89,7 @@ const { ethers, upgrades } = require("hardhat"); console.log("\nDeployment arguments loaded:", { contractController, admin, + beTaxOpsWallets: beTaxOpsWalletList, assetToken, taxToken, agentTaxDexRouter, @@ -107,6 +129,9 @@ const { ethers, upgrades } = require("hardhat"); const tx = await agentNftV2.grantRole(defaultAdminRole, admin); await tx.wait(); console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); + + // Verify AgentNftV2 proxy + await verifyContract(agentNftV2Address!); } else { console.log("\n--- Using existing AgentNftV2 ---"); console.log("AgentNftV2 address:", agentNftV2Address); @@ -143,6 +168,25 @@ const { ethers, upgrades } = require("hardhat"); agentTaxAddress = await agentTax.getAddress(); console.log("AgentTax deployed at:", agentTaxAddress); deployedContracts["AGENT_TOKEN_TAX_MANAGER"] = agentTaxAddress; + + // Grant EXECUTOR_V2_ROLE to all BE_TAX_OPS_WALLETS + const executorV2Role = await agentTax.EXECUTOR_V2_ROLE(); + for (const wallet of beTaxOpsWalletList) { + const grantTx = await agentTax.grantRole(executorV2Role, wallet); + await grantTx.wait(); + console.log("EXECUTOR_V2_ROLE granted to:", wallet); + } + + // Grant EXECUTOR_ROLE to all BE_HANDLE_AGENT_TAXES_WALLETS + const executorRole = await agentTax.EXECUTOR_ROLE(); + for (const wallet of beHandleAgentTaxesWalletList) { + const grantTx = await agentTax.grantRole(executorRole, wallet); + await grantTx.wait(); + console.log("EXECUTOR_ROLE granted to:", wallet); + } + + // Verify AgentTax proxy + await verifyContract(agentTaxAddress); } else { console.log("\n--- Using existing AgentTax ---"); console.log("AgentTax address:", existingAgentTax); @@ -179,7 +223,8 @@ const { ethers, upgrades } = require("hardhat"); console.log("\n--- Manual Steps Required (by admin) ---"); console.log("1. AgentNftV2: MINTER_ROLE will be granted to AgentFactoryV6 in deployLaunchpadv5_2.ts"); - console.log("2. AgentTax: Admin may need to grant EXECUTOR_ROLE and EXECUTOR_V2_ROLE to executor address"); + console.log(`2. AgentTax: EXECUTOR_V2_ROLE has been granted to ${beTaxOpsWalletList.length} wallet(s)`); + console.log("3. AgentTax: Admin may need to grant EXECUTOR_ROLE to executor address for handleAgentTaxes()"); console.log("\n--- Next Step ---"); console.log("Run: npx hardhat run scripts/launchpadv5/deployLaunchpadv5_1.ts --network "); diff --git a/scripts/launchpadv5/deployLaunchpadv5_1.ts b/scripts/launchpadv5/deployLaunchpadv5_1.ts index 80b6837..05bc1bb 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_1.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_1.ts @@ -1,4 +1,5 @@ import { parseEther } from "ethers"; +import { verifyContract } from "./utils"; const { ethers, upgrades } = require("hardhat"); // Environment variables are loaded via hardhat.config.js @@ -114,6 +115,9 @@ const { ethers, upgrades } = require("hardhat"); fFactoryV2Address = await fFactoryV2.getAddress(); deployedContracts.FFactoryV2 = fFactoryV2Address; console.log("FFactoryV2 deployed at:", fFactoryV2Address); + + // Verify FFactoryV2 proxy + await verifyContract(fFactoryV2Address); } // ============================================ @@ -143,6 +147,9 @@ const { ethers, upgrades } = require("hardhat"); fRouterV2Address = await fRouterV2.getAddress(); deployedContracts.FRouterV2 = fRouterV2Address; console.log("FRouterV2 deployed at:", fRouterV2Address); + + // Verify FRouterV2 proxy + await verifyContract(fRouterV2Address); } // If both contracts were reused, skip configuration @@ -163,18 +170,22 @@ const { ethers, upgrades } = require("hardhat"); const defaultAdminRole = await fFactoryV2.DEFAULT_ADMIN_ROLE(); // Grant ADMIN_ROLE to deployer temporarily - await fFactoryV2.grantRole(adminRole, deployerAddress); + const tx1 = await fFactoryV2.grantRole(adminRole, deployerAddress); + await tx1.wait(); console.log("ADMIN_ROLE granted to deployer temporarily"); // Set Router - await fFactoryV2.setRouter(fRouterV2Address); + const tx2 = await fFactoryV2.setRouter(fRouterV2Address); + await tx2.wait(); console.log("Router set in FFactoryV2"); // Grant roles to admin - await fFactoryV2.grantRole(adminRole, admin); + const tx3 = await fFactoryV2.grantRole(adminRole, admin); + await tx3.wait(); console.log("ADMIN_ROLE granted to admin:", admin); - await fFactoryV2.grantRole(defaultAdminRole, admin); + const tx4 = await fFactoryV2.grantRole(defaultAdminRole, admin); + await tx4.wait(); console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); } @@ -185,16 +196,19 @@ const { ethers, upgrades } = require("hardhat"); console.log("\n--- Configuring FRouterV2 ---"); // Grant ADMIN_ROLE to admin (needed for setBondingV5) - await fRouterV2.grantRole(await fRouterV2.ADMIN_ROLE(), admin); + const tx5 = await fRouterV2.grantRole(await fRouterV2.ADMIN_ROLE(), admin); + await tx5.wait(); console.log("ADMIN_ROLE granted to admin on FRouterV2"); // Grant DEFAULT_ADMIN_ROLE to admin - await fRouterV2.grantRole(await fRouterV2.DEFAULT_ADMIN_ROLE(), admin); + const tx6 = await fRouterV2.grantRole(await fRouterV2.DEFAULT_ADMIN_ROLE(), admin); + await tx6.wait(); console.log("DEFAULT_ADMIN_ROLE granted to admin on FRouterV2"); // Grant EXECUTOR_ROLE to BE_OPS_WALLET (for resetTime) const executorRole = await fRouterV2.EXECUTOR_ROLE(); - await fRouterV2.grantRole(executorRole, beOpsWallet); + const tx7 = await fRouterV2.grantRole(executorRole, beOpsWallet); + await tx7.wait(); console.log("EXECUTOR_ROLE granted to BE_OPS_WALLET:", beOpsWallet); } diff --git a/scripts/launchpadv5/deployLaunchpadv5_2.ts b/scripts/launchpadv5/deployLaunchpadv5_2.ts index 324dc5f..afa7631 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_2.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_2.ts @@ -1,4 +1,5 @@ import { parseEther } from "ethers"; +import { verifyContract } from "./utils"; const { ethers, upgrades } = require("hardhat"); // Environment variables are loaded via hardhat.config.js @@ -157,6 +158,9 @@ const { ethers, upgrades } = require("hardhat"); "AgentTokenV2 implementation deployed at:", agentTokenV2ImplAddress ); + + // Verify AgentTokenV2 implementation + await verifyContract(agentTokenV2ImplAddress); } else { agentTokenV2ImplAddress = agentTokenV2Impl; console.log( @@ -181,6 +185,9 @@ const { ethers, upgrades } = require("hardhat"); "AgentVeTokenV2 implementation deployed at:", agentVeTokenV2ImplAddress ); + + // Verify AgentVeTokenV2 implementation + await verifyContract(agentVeTokenV2ImplAddress); } else { agentVeTokenV2ImplAddress = agentVeTokenV2Impl; console.log( @@ -202,6 +209,9 @@ const { ethers, upgrades } = require("hardhat"); agentDAOImplAddress = await agentDAO.getAddress(); deployedContracts.AgentDAOImpl = agentDAOImplAddress; console.log("AgentDAO implementation deployed at:", agentDAOImplAddress); + + // Verify AgentDAO implementation + await verifyContract(agentDAOImplAddress); } else { agentDAOImplAddress = agentDAOImpl; console.log( @@ -253,37 +263,43 @@ const { ethers, upgrades } = require("hardhat"); deployedContracts.AgentFactoryV6 = agentFactoryV6Address; console.log("AgentFactoryV6 deployed at:", agentFactoryV6Address); + // Verify AgentFactoryV6 proxy + await verifyContract(agentFactoryV6Address); + // ============================================ // 8. Configure AgentFactoryV6 // ============================================ console.log("\n--- Configuring AgentFactoryV6 ---"); // Grant DEFAULT_ADMIN_ROLE to deployer temporarily - await agentFactoryV6.grantRole( + const txGrantAdmin = await agentFactoryV6.grantRole( await agentFactoryV6.DEFAULT_ADMIN_ROLE(), deployerAddress ); + await txGrantAdmin.wait(); console.log("DEFAULT_ADMIN_ROLE granted to deployer temporarily"); // Set params - await agentFactoryV6.setParams( + const txSetParams = await agentFactoryV6.setParams( agentFactoryV6MaturityDuration, // maturity duration (from env) uniswapV2RouterAddress, admin, // defaultDelegatee admin // tokenAdmin ); + await txSetParams.wait(); console.log( "setParams() called for AgentFactoryV6 with maturityDuration:", agentFactoryV6MaturityDuration ); // Set token params (Sentient phase tax configuration) - await agentFactoryV6.setTokenParams( + const txSetTokenParams = await agentFactoryV6.setTokenParams( sentientBuyTax, // projectBuyTaxBasisPoints sentientSellTax, // projectSellTaxBasisPoints taxSwapThresholdBasisPoints, // taxSwapThresholdBasisPoints (from env) agentTokenTaxManager // projectTaxRecipient (fee address) ); + await txSetTokenParams.wait(); console.log("setTokenParams() called for AgentFactoryV6:", { sentientBuyTax, sentientSellTax, @@ -292,15 +308,17 @@ const { ethers, upgrades } = require("hardhat"); }); // Grant DEFAULT_ADMIN_ROLE to admin - await agentFactoryV6.grantRole( + const txGrantAdminToAdmin = await agentFactoryV6.grantRole( await agentFactoryV6.DEFAULT_ADMIN_ROLE(), admin ); + await txGrantAdminToAdmin.wait(); console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); // Grant REMOVE_LIQUIDITY_ROLE to admin const removeLiqRole = await agentFactoryV6.REMOVE_LIQUIDITY_ROLE(); - await agentFactoryV6.grantRole(removeLiqRole, admin); + const txGrantRemoveLiq = await agentFactoryV6.grantRole(removeLiqRole, admin); + await txGrantRemoveLiq.wait(); console.log("REMOVE_LIQUIDITY_ROLE granted to admin:", admin); // ============================================ diff --git a/scripts/launchpadv5/deployLaunchpadv5_3.ts b/scripts/launchpadv5/deployLaunchpadv5_3.ts index d64c729..8fe8c19 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_3.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_3.ts @@ -1,4 +1,5 @@ import { parseEther } from "ethers"; +import { verifyContract } from "./utils"; const { ethers, upgrades } = require("hardhat"); // Environment variables are loaded via hardhat.config.js @@ -151,6 +152,9 @@ const { ethers, upgrades } = require("hardhat"); const bondingConfigAddress = await bondingConfig.getAddress(); console.log("BondingConfig deployed at:", bondingConfigAddress); + // Verify BondingConfig proxy + await verifyContract(bondingConfigAddress); + // Set X Launchers const xLauncherAddresses = process.env.X_LAUNCHER_ADDRESSES; if (xLauncherAddresses) { @@ -197,6 +201,9 @@ const { ethers, upgrades } = require("hardhat"); const bondingV5Address = await bondingV5.getAddress(); console.log("BondingV5 deployed at:", bondingV5Address); + // Verify BondingV5 proxy + await verifyContract(bondingV5Address); + // ============================================ // 3. Transfer Ownership // ============================================ @@ -218,6 +225,10 @@ const { ethers, upgrades } = require("hardhat"); console.log("\n--- Granting necessary roles (using admin wallet) ---"); const adminPrivateKey = process.env.ADMIN_PRIVATE_KEY; + const agentTaxAddress = process.env.AGENT_TOKEN_TAX_MANAGER; + if (!agentTaxAddress) { + throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + } if (!adminPrivateKey) { console.log("\n" + "=".repeat(80)); console.log("⚠️ ADMIN_PRIVATE_KEY not set - Manual role grants required!"); @@ -227,6 +238,7 @@ const { ethers, upgrades } = require("hardhat"); console.log(`2. FRouterV2.setBondingV5(${bondingV5Address}, ${bondingConfigAddress})`); console.log(`3. FRouterV2.grantRole(EXECUTOR_ROLE, ${bondingV5Address})`); console.log(`4. AgentFactoryV6.grantRole(BONDING_ROLE, ${bondingV5Address})`); + console.log(`5. AgentTax.setBondingV5(${bondingV5Address})`); console.log("=".repeat(80)); throw new Error("ADMIN_PRIVATE_KEY not set - Manual role grants required!"); } else { @@ -260,6 +272,19 @@ const { ethers, upgrades } = require("hardhat"); await (await agentFactoryV6.grantRole(bondingRole, bondingV5Address)).wait(); console.log("✅ Granted BONDING_ROLE of AgentFactoryV6 to BondingV5"); + // 3.5 Set BondingV5 in AgentTax (optional - only if AGENT_TAX_ADDRESS is set) + const agentTaxAddress = process.env.AGENT_TAX_ADDRESS; + if (agentTaxAddress) { + console.log("\n--- Setting BondingV5 in AgentTax ---"); + const agentTax = await ethers.getContractAt("AgentTax", agentTaxAddress, adminSigner); + await (await agentTax.setBondingV5(bondingV5Address)).wait(); + console.log("✅ Set BondingV5 in AgentTax:", bondingV5Address); + } else { + console.log("\n⚠️ AGENT_TAX_ADDRESS not set - skipping AgentTax.setBondingV5()"); + console.log(" If you have AgentTax deployed, run manually:"); + console.log(` agentTax.setBondingV5(${bondingV5Address})`); + } + console.log("\n✅ All role grants completed!"); } diff --git a/scripts/launchpadv5/deploy_agent_tax_swap.ts b/scripts/launchpadv5/deploy_agent_tax_swap.ts new file mode 100644 index 0000000..1b1353f --- /dev/null +++ b/scripts/launchpadv5/deploy_agent_tax_swap.ts @@ -0,0 +1,331 @@ +// npx hardhat run scripts/launchpadv5/deploy_tax_token_and_test_swap.ts --network eth_sepolia +import { parseEther, formatEther } from "ethers"; +const { ethers, run } = require("hardhat"); + +// Uniswap V2 Router ABI (only the functions we need) +const UNISWAP_V2_ROUTER_ABI = [ + "function factory() external view returns (address)", + "function WETH() external view returns (address)", + "function addLiquidity(address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin, address to, uint deadline) external returns (uint amountA, uint amountB, uint liquidity)", + "function swapExactTokensForTokens(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) external returns (uint[] memory amounts)", + "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)", +]; + +// Uniswap V2 Factory ABI +const UNISWAP_V2_FACTORY_ABI = [ + "function getPair(address tokenA, address tokenB) external view returns (address pair)", + "function createPair(address tokenA, address tokenB) external returns (address pair)", +]; + +// ERC20 ABI +const ERC20_ABI = [ + "function name() external view returns (string)", + "function symbol() external view returns (string)", + "function decimals() external view returns (uint8)", + "function totalSupply() external view returns (uint256)", + "function balanceOf(address account) external view returns (uint256)", + "function approve(address spender, uint256 amount) external returns (bool)", + "function allowance(address owner, address spender) external view returns (uint256)", + "function transfer(address to, uint256 amount) external returns (bool)", +]; + +async function main() { + console.log("\n" + "=".repeat(80)); + console.log(" Deploy Tax Token and Test Uniswap V2 Swap"); + console.log("=".repeat(80)); + + const [deployer] = await ethers.getSigners(); + const deployerAddress = await deployer.getAddress(); + console.log("Deployer address:", deployerAddress); + + // Load required environment variables + const virtualTokenAddress = process.env.VIRTUAL_TOKEN_ADDRESS; + if (!virtualTokenAddress) { + throw new Error("VIRTUAL_TOKEN_ADDRESS not set in environment"); + } + const uniswapV2RouterAddress = process.env.UNISWAP_V2_ROUTER; + if (!uniswapV2RouterAddress) { + throw new Error("UNISWAP_V2_ROUTER not set in environment"); + } + + console.log("\n--- Configuration ---"); + console.log("VIRTUAL_TOKEN_ADDRESS:", virtualTokenAddress); + console.log("UNISWAP_V2_ROUTER:", uniswapV2RouterAddress); + + // Get VIRTUAL token contract + const virtualToken = new ethers.Contract(virtualTokenAddress, ERC20_ABI, deployer); + const virtualSymbol = await virtualToken.symbol(); + const virtualDecimals = await virtualToken.decimals(); + console.log(`VIRTUAL Token: ${virtualSymbol} (${virtualDecimals} decimals)`); + + // Check deployer's VIRTUAL balance + const virtualBalance = await virtualToken.balanceOf(deployerAddress); + console.log(`Deployer VIRTUAL balance: ${formatEther(virtualBalance)} ${virtualSymbol}`); + + if (virtualBalance < parseEther("1000")) { + throw new Error("Deployer needs at least 1000 VIRTUAL tokens for this test"); + } + + // ============================================ + // Step 1: Deploy Tax Token (MockERC20) + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 1: Deploy Tax Token (MockERC20)"); + console.log("=".repeat(80)); + + // Check if TAX_TOKEN already exists in env + let taxTokenAddress = process.env.AGENT_TAX_TOKEN; + let taxToken: any; + + if (taxTokenAddress) { + console.log("Using existing AGENT_TAX_TOKEN:", taxTokenAddress); + taxToken = await ethers.getContractAt("MockERC20", taxTokenAddress); + } else { + console.log("Deploying new MockERC20 as Tax Token..."); + console.log("Note: Anyone can mint tokens via mint(address, amount)"); + + const MockERC20 = await ethers.getContractFactory("MockERC20"); + + // Deploy with zero initial balance - we'll mint as needed + const taxTokenContract = await MockERC20.deploy( + "Fake WETH", + "WETH", + deployerAddress, + 0 // zero initial supply + ); + await taxTokenContract.waitForDeployment(); + taxTokenAddress = await taxTokenContract.getAddress(); + taxToken = taxTokenContract; + + console.log("Tax Token deployed at:", taxTokenAddress); + + // Verify the contract + console.log("\n--- Verifying Tax Token on Etherscan ---"); + try { + await run("verify:verify", { + address: taxTokenAddress, + constructorArguments: [ + "Test Tax Token", + "TAX", + deployerAddress, + "0", + ], + }); + console.log("✅ Tax Token verified on Etherscan"); + } catch (error: any) { + if (error.message.includes("Already Verified")) { + console.log("✅ Tax Token already verified"); + } else { + console.log("⚠️ Verification failed:", error.message); + } + } + } + + const taxSymbol = await taxToken.symbol(); + let taxBalance = await taxToken.balanceOf(deployerAddress); + console.log(`Tax Token: ${taxSymbol}`); + console.log(`Deployer TAX balance: ${formatEther(taxBalance)} ${taxSymbol}`); + + // Mint TAX tokens if balance is low (need at least 1000 for liquidity + tests) + const minRequiredTax = parseEther("1000"); + if (taxBalance < minRequiredTax) { + console.log("\n--- Minting TAX tokens ---"); + const mintAmount = parseEther("1000000"); // Mint 1M TAX tokens + const mintTx = await taxToken.mint(deployerAddress, mintAmount); + await mintTx.wait(); + taxBalance = await taxToken.balanceOf(deployerAddress); + console.log(`✅ Minted ${formatEther(mintAmount)} TAX tokens`); + console.log(`New TAX balance: ${formatEther(taxBalance)} ${taxSymbol}`); + } + + // ============================================ + // Step 2: Setup Uniswap V2 Router and Factory + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 2: Setup Uniswap V2 Liquidity Pool"); + console.log("=".repeat(80)); + + const uniswapRouter = new ethers.Contract(uniswapV2RouterAddress, UNISWAP_V2_ROUTER_ABI, deployer); + const factoryAddress = await uniswapRouter.factory(); + console.log("Uniswap V2 Factory:", factoryAddress); + + const uniswapFactory = new ethers.Contract(factoryAddress, UNISWAP_V2_FACTORY_ABI, deployer); + + // Check if pair already exists + let pairAddress = await uniswapFactory.getPair(virtualTokenAddress, taxTokenAddress); + console.log("Existing pair address:", pairAddress); + + // Amount of liquidity to add + const virtualLiquidityAmount = parseEther("500"); // 500 VIRTUAL + const taxLiquidityAmount = parseEther("500"); // 500 TAX (1:1 ratio) + + // Approve tokens for router + console.log("\n--- Approving tokens for Uniswap Router ---"); + + const virtualAllowance = await virtualToken.allowance(deployerAddress, uniswapV2RouterAddress); + if (virtualAllowance < virtualLiquidityAmount) { + const approveTx1 = await virtualToken.approve(uniswapV2RouterAddress, ethers.MaxUint256); + await approveTx1.wait(); + console.log("✅ Approved VIRTUAL tokens"); + } else { + console.log("✅ VIRTUAL already approved"); + } + + const taxAllowance = await taxToken.allowance(deployerAddress, uniswapV2RouterAddress); + if (taxAllowance < taxLiquidityAmount) { + const approveTx2 = await taxToken.approve(uniswapV2RouterAddress, ethers.MaxUint256); + await approveTx2.wait(); + console.log("✅ Approved TAX tokens"); + } else { + console.log("✅ TAX already approved"); + } + + // Add liquidity if pair doesn't exist or has no liquidity + if (pairAddress === ethers.ZeroAddress) { + console.log("\n--- Adding initial liquidity ---"); + console.log(`Adding ${formatEther(virtualLiquidityAmount)} VIRTUAL + ${formatEther(taxLiquidityAmount)} TAX`); + + const deadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now + + const addLiquidityTx = await uniswapRouter.addLiquidity( + virtualTokenAddress, + taxTokenAddress, + virtualLiquidityAmount, + taxLiquidityAmount, + 0, // amountAMin (accept any) + 0, // amountBMin (accept any) + deployerAddress, + deadline + ); + const receipt = await addLiquidityTx.wait(); + console.log("✅ Liquidity added! Gas used:", receipt.gasUsed.toString()); + + // Get the new pair address + pairAddress = await uniswapFactory.getPair(virtualTokenAddress, taxTokenAddress); + console.log("New pair address:", pairAddress); + } else { + console.log("✅ Pair already exists, skipping liquidity addition"); + } + + // ============================================ + // Step 3: Swap VIRTUAL -> TAX + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 3: Swap 100 VIRTUAL -> TAX"); + console.log("=".repeat(80)); + + const swapAmountIn = parseEther("100"); // 100 VIRTUAL + const path1 = [virtualTokenAddress, taxTokenAddress]; + + // Get expected output + const amountsOut1 = await uniswapRouter.getAmountsOut(swapAmountIn, path1); + const expectedTaxOut = amountsOut1[1]; + console.log(`Swapping ${formatEther(swapAmountIn)} VIRTUAL`); + console.log(`Expected TAX output: ${formatEther(expectedTaxOut)} TAX`); + + // Record balances before swap + const virtualBalanceBefore1 = await virtualToken.balanceOf(deployerAddress); + const taxBalanceBefore1 = await taxToken.balanceOf(deployerAddress); + + // Execute swap + const deadline1 = Math.floor(Date.now() / 1000) + 3600; + const swap1Tx = await uniswapRouter.swapExactTokensForTokens( + swapAmountIn, + 0, // amountOutMin (accept any for test) + path1, + deployerAddress, + deadline1 + ); + const swap1Receipt = await swap1Tx.wait(); + console.log("✅ Swap executed! Gas used:", swap1Receipt.gasUsed.toString()); + + // Record balances after swap + const virtualBalanceAfter1 = await virtualToken.balanceOf(deployerAddress); + const taxBalanceAfter1 = await taxToken.balanceOf(deployerAddress); + + const virtualSpent = virtualBalanceBefore1 - virtualBalanceAfter1; + const taxReceived = taxBalanceAfter1 - taxBalanceBefore1; + + console.log("\n--- Swap 1 Results ---"); + console.log(`VIRTUAL spent: ${formatEther(virtualSpent)} ${virtualSymbol}`); + console.log(`TAX received: ${formatEther(taxReceived)} ${taxSymbol}`); + + // ============================================ + // Step 4: Swap TAX -> VIRTUAL (swap back) + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 4: Swap TAX -> VIRTUAL (swap back)"); + console.log("=".repeat(80)); + + const swapAmountIn2 = taxReceived; // Swap all the TAX we received + const path2 = [taxTokenAddress, virtualTokenAddress]; + + // Get expected output + const amountsOut2 = await uniswapRouter.getAmountsOut(swapAmountIn2, path2); + const expectedVirtualOut = amountsOut2[1]; + console.log(`Swapping ${formatEther(swapAmountIn2)} TAX`); + console.log(`Expected VIRTUAL output: ${formatEther(expectedVirtualOut)} VIRTUAL`); + + // Record balances before swap + const virtualBalanceBefore2 = await virtualToken.balanceOf(deployerAddress); + const taxBalanceBefore2 = await taxToken.balanceOf(deployerAddress); + + // Execute swap + const deadline2 = Math.floor(Date.now() / 1000) + 3600; + const swap2Tx = await uniswapRouter.swapExactTokensForTokens( + swapAmountIn2, + 0, // amountOutMin (accept any for test) + path2, + deployerAddress, + deadline2 + ); + const swap2Receipt = await swap2Tx.wait(); + console.log("✅ Swap executed! Gas used:", swap2Receipt.gasUsed.toString()); + + // Record balances after swap + const virtualBalanceAfter2 = await virtualToken.balanceOf(deployerAddress); + const taxBalanceAfter2 = await taxToken.balanceOf(deployerAddress); + + const taxSpent = taxBalanceBefore2 - taxBalanceAfter2; + const virtualReceived = virtualBalanceAfter2 - virtualBalanceBefore2; + + console.log("\n--- Swap 2 Results ---"); + console.log(`TAX spent: ${formatEther(taxSpent)} ${taxSymbol}`); + console.log(`VIRTUAL received: ${formatEther(virtualReceived)} ${virtualSymbol}`); + + // ============================================ + // Summary + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Summary"); + console.log("=".repeat(80)); + + console.log("\n--- Deployed Contracts ---"); + console.log(`AGENT_TAX_TOKEN=${taxTokenAddress}`); + + console.log("\n--- Final Balances ---"); + const finalVirtualBalance = await virtualToken.balanceOf(deployerAddress); + const finalTaxBalance = await taxToken.balanceOf(deployerAddress); + console.log(`VIRTUAL: ${formatEther(finalVirtualBalance)} ${virtualSymbol}`); + console.log(`TAX: ${formatEther(finalTaxBalance)} ${taxSymbol}`); + + console.log("\n--- Swap Summary ---"); + console.log(`Swap 1: ${formatEther(virtualSpent)} VIRTUAL -> ${formatEther(taxReceived)} TAX`); + console.log(`Swap 2: ${formatEther(taxSpent)} TAX -> ${formatEther(virtualReceived)} VIRTUAL`); + + const netVirtualLoss = virtualSpent - virtualReceived; + console.log(`Net VIRTUAL loss (due to AMM fees): ${formatEther(netVirtualLoss)} VIRTUAL`); + + console.log("\n" + "=".repeat(80)); + console.log(" ✅ Test Complete!"); + console.log("=".repeat(80)); + console.log("\nAdd to your .env file:"); + console.log(`AGENT_TAX_TOKEN=${taxTokenAddress}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error("❌ Test Failed:", error); + process.exit(1); + }); diff --git a/scripts/launchpadv5/handle_agent_tax.ts b/scripts/launchpadv5/handle_agent_tax.ts new file mode 100644 index 0000000..9f1d603 --- /dev/null +++ b/scripts/launchpadv5/handle_agent_tax.ts @@ -0,0 +1,180 @@ +/** + * Script to call AgentTax.handleAgentTaxes() to distribute tax to creator and treasury + * + * Usage: + * npx hardhat run scripts/launchpadv5/handle_agent_tax.ts --network + * + * Before running: + * 1. Set the correct .env file (e.g., .env.launchpadv5_dev_eth_sepolia) + * 2. Update the hardcoded parameters below + */ + +import { ethers } from "hardhat"; + +// AgentTax contract ABI (only the function we need) +const AGENT_TAX_ABI = [ + "function handleAgentTaxes(uint256 agentId, bytes32[] memory txhashes, uint256[] memory amounts, uint256 minOutput) external", + "function agentTaxAmounts(uint256 agentId) view returns (uint256 amountCollected, uint256 amountSwapped)", + "function taxHistory(bytes32 txhash) view returns (uint256 agentId, uint256 amount)", + "function EXECUTOR_ROLE() view returns (bytes32)", + "function hasRole(bytes32 role, address account) view returns (bool)", +]; + +async function main() { + console.log("\n=== Handle Agent Tax Script ===\n"); + + // ============================================ + // CONFIGURATION - UPDATE THESE VALUES + // ============================================ + + /** + * AGENT_TAX_ADDRESS: The deployed AgentTax contract address + * Where to get: From your deployment output or .env file (e.g., AGENT_TAX_ADDRESS) + */ + const AGENT_TAX_ADDRESS = process.env.AGENT_TOKEN_TAX_MANAGER; + if (!AGENT_TAX_ADDRESS) { + throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + } + + /** + * AGENT_ID: The agent/virtual ID (NFT token ID from AgentNft contract) + * Where to get: + * - From AgentNft contract - use tokenOfOwnerByIndex() or check Transfer events + * - From AgentFactoryV6 NewPersona event - the virtualId parameter + * - From AgentFactoryV6.applications(applicationId).virtualId - after application is executed + * + * NOTE: This is NOT the same as BondingV5.tokenInfo(token).virtualId which starts at 50_000_000_000! + * The AGENT_ID here is the real NFT token ID (1, 2, 3, etc.) from AgentNft. + */ + const AGENT_ID = 1050000000001; // TODO: Fill in the agent ID (NFT token ID from AgentNft) + + /** + * TX_HASHES: Array of transaction hashes that collected tax + * Where to get: + * - From blockchain explorer (buy/sell transactions on the token) + * - From backend database tracking tax collection + * - Each txhash can only be used once (contract will revert if duplicate) + * Format: bytes32 array (use ethers.id() or keccak256 to convert string to bytes32) + */ + const TX_HASHES: string[] = [ + ethers.id("0xda40edb25e65be793c6b314848ac131c85c40e136082e156ceb4f146f4c7a232"), // TODO: Replace with actual tx hashes + // ethers.id("0x6d2c9fa49f51324907dbc80d0084cbb8194aa50afed85c1f8b80e9c1baf2efef"), // Example: ethers.id("0x1234...abcd") + ]; + + /** + * AMOUNTS: Array of tax amounts corresponding to each txhash (in wei) + * Where to get: + * - From backend calculation based on buy/sell amounts and tax rate + * - Must match the length of TX_HASHES array + * Format: BigInt in wei (e.g., ethers.parseEther("10") for 10 tokens) + */ + const AMOUNTS: bigint[] = [ + ethers.parseEther("1.01"), // TODO: Replace with actual amounts + // ethers.parseEther("0.5"), + ]; + + /** + * MIN_OUTPUT: Minimum output amount from the swap (slippage protection) + * Where to get: + * - Calculate based on expected swap output minus slippage tolerance + * - Can use router.getAmountsOut() to estimate + * - Set to 0 for testing (not recommended for production) + */ + const MIN_OUTPUT = ethers.parseEther("0"); // TODO: Set appropriate slippage protection + + // ============================================ + // SCRIPT EXECUTION + // ============================================ + + // Validate inputs + if (TX_HASHES.length !== AMOUNTS.length) { + throw new Error("TX_HASHES and AMOUNTS arrays must have the same length"); + } + + if (TX_HASHES.length === 0) { + throw new Error("TX_HASHES array cannot be empty"); + } + + // Get signer + const [signer] = await ethers.getSigners(); + console.log("Signer address:", signer.address); + + // Connect to AgentTax contract + const agentTax = new ethers.Contract(AGENT_TAX_ADDRESS, AGENT_TAX_ABI, signer); + console.log("AgentTax address:", AGENT_TAX_ADDRESS); + + // Check if signer has EXECUTOR_ROLE + const executorRole = await agentTax.EXECUTOR_ROLE(); + const hasRole = await agentTax.hasRole(executorRole, signer.address); + console.log("Signer has EXECUTOR_ROLE:", hasRole); + + if (!hasRole) { + throw new Error("Signer does not have EXECUTOR_ROLE on AgentTax contract"); + } + + // Check current tax amounts for this agent + console.log("\n--- Current Tax Amounts for Agent", AGENT_ID, "---"); + const taxAmounts = await agentTax.agentTaxAmounts(AGENT_ID); + console.log("Amount Collected:", ethers.formatEther(taxAmounts.amountCollected)); + console.log("Amount Swapped:", ethers.formatEther(taxAmounts.amountSwapped)); + + // Check if any txhash already exists + console.log("\n--- Checking TX Hash History ---"); + for (let i = 0; i < TX_HASHES.length; i++) { + const history = await agentTax.taxHistory(TX_HASHES[i]); + if (history.agentId > 0) { + console.log(`WARNING: TX_HASH[${i}] already used for agentId ${history.agentId}`); + } else { + console.log(`TX_HASH[${i}]: OK (not used)`); + } + } + + // Calculate total amount + const totalAmount = AMOUNTS.reduce((a, b) => a + b, 0n); + console.log("\n--- Transaction Summary ---"); + console.log("Agent ID:", AGENT_ID); + console.log("Number of transactions:", TX_HASHES.length); + console.log("Total tax amount:", ethers.formatEther(totalAmount)); + console.log("Min output:", ethers.formatEther(MIN_OUTPUT)); + + // Confirm before executing + console.log("\n⚠️ About to call handleAgentTaxes()..."); + console.log("Press Ctrl+C to cancel, or wait 5 seconds to continue...\n"); + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Execute the transaction + console.log("--- Executing handleAgentTaxes() ---"); + try { + const tx = await agentTax.handleAgentTaxes(AGENT_ID, TX_HASHES, AMOUNTS, MIN_OUTPUT); + console.log("Transaction hash:", tx.hash); + console.log("Waiting for confirmation..."); + + const receipt = await tx.wait(); + console.log("✅ Transaction confirmed in block:", receipt.blockNumber); + console.log("Gas used:", receipt.gasUsed.toString()); + + // Check updated tax amounts + console.log("\n--- Updated Tax Amounts for Agent", AGENT_ID, "---"); + const updatedTaxAmounts = await agentTax.agentTaxAmounts(AGENT_ID); + console.log("Amount Collected:", ethers.formatEther(updatedTaxAmounts.amountCollected)); + console.log("Amount Swapped:", ethers.formatEther(updatedTaxAmounts.amountSwapped)); + + } catch (error: any) { + console.error("❌ Transaction failed:", error.message); + + // Try to get more details + if (error.data) { + console.error("Error data:", error.data); + } + throw error; + } + + console.log("\n=== Script Completed ===\n"); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/launchpadv5/utils.ts b/scripts/launchpadv5/utils.ts new file mode 100644 index 0000000..cdb4e7f --- /dev/null +++ b/scripts/launchpadv5/utils.ts @@ -0,0 +1,22 @@ +const { run } = require("hardhat"); + +/** + * Verify a contract on Etherscan/Basescan + * Handles "Already Verified" errors gracefully + */ +export async function verifyContract(address: string, constructorArguments: any[] = []) { + console.log(`\n--- Verifying contract at ${address} ---`); + try { + await run("verify:verify", { + address, + constructorArguments, + }); + console.log("✅ Contract verified"); + } catch (error: any) { + if (error.message.includes("Already Verified")) { + console.log("✅ Contract already verified"); + } else { + console.log("⚠️ Verification failed:", error.message); + } + } +} diff --git a/test/launchpadv5/bondingV5.js b/test/launchpadv5/bondingV5.js index 29ea287..a2c9664 100644 --- a/test/launchpadv5/bondingV5.js +++ b/test/launchpadv5/bondingV5.js @@ -861,16 +861,16 @@ describe("BondingV5", function () { }); // ============================================ - // PEGASUS_X_LAUNCH Mode Tests + // LAUNCH_MODE_X_LAUNCH (Special Mode) Tests // ============================================ - describe("PEGASUS_X_LAUNCH Mode", function () { + describe("LAUNCH_MODE_X_LAUNCH (Special Mode)", function () { before(async function () { const { bondingConfig } = contracts; const { owner, user1 } = accounts; - // Authorize user1 as XLauncher + // Authorize user1 as XLauncher for X_LAUNCH mode await bondingConfig.connect(owner).setXLauncher(user1.address, true); - console.log("user1 authorized as XLauncher for PEGASUS_X_LAUNCH"); + console.log("user1 authorized as XLauncher for LAUNCH_MODE_X_LAUNCH"); }); it("Should create a token with isProjectXLaunch returning true", async function () { @@ -881,7 +881,7 @@ describe("BondingV5", function () { await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); - // Pegasus modes require immediate launch (startTime within 24h) + // Special modes require immediate launch (startTime within 24h) const startTime = (await time.latest()) + 100; const tx = await bondingV5 .connect(user1) @@ -895,10 +895,10 @@ describe("BondingV5", function () { purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, - 0, // airdropPercent must be 0 for Pegasus - false, // needAcf must be false for Pegasus - ANTI_SNIPER_NONE, // antiSniperTaxType must be NONE for Pegasus - false // isProject60days must be false for Pegasus + 0, // airdropPercent must be 0 for special modes + false, // needAcf must be false for special modes + ANTI_SNIPER_NONE, // antiSniperTaxType must be NONE for special modes + false // isProject60days must be false for special modes ); const receipt = await tx.wait(); @@ -957,7 +957,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "UnauthorizedLauncher"); }); - it("Should revert if Pegasus mode uses invalid params", async function () { + it("Should revert if X_LAUNCH mode uses invalid params", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -966,7 +966,7 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + 100; - // Should revert with non-zero airdropPercent (even if within maxAirdropPercent) + // Should revert with non-zero airdropPercent (special modes require 0) await expect( bondingV5.connect(user1).preLaunch( "Invalid X Launch", @@ -988,16 +988,16 @@ describe("BondingV5", function () { }); // ============================================ - // PEGASUS_ACP_SKILL Mode Tests + // LAUNCH_MODE_ACP_SKILL (Special Mode) Tests // ============================================ - describe("PEGASUS_ACP_SKILL Mode", function () { + describe("LAUNCH_MODE_ACP_SKILL (Special Mode)", function () { before(async function () { const { bondingConfig } = contracts; const { owner, user1 } = accounts; - // Authorize user1 as AcpSkillLauncher + // Authorize user1 as AcpSkillLauncher for ACP_SKILL mode await bondingConfig.connect(owner).setAcpSkillLauncher(user1.address, true); - console.log("user1 authorized as AcpSkillLauncher for PEGASUS_ACP_SKILL"); + console.log("user1 authorized as AcpSkillLauncher for LAUNCH_MODE_ACP_SKILL"); }); it("Should create a token with isAcpSkillLaunch returning true", async function () { @@ -1634,9 +1634,9 @@ describe("BondingV5", function () { }); // ============================================ - // Pegasus Mode Strict Validation Tests + // Special Mode Strict Validation Tests (X_LAUNCH and ACP_SKILL) // ============================================ - describe("Pegasus Mode Strict Validation", function () { + describe("Special Mode Strict Validation (X_LAUNCH and ACP_SKILL)", function () { before(async function () { const { bondingConfig } = contracts; const { owner, user1 } = accounts; @@ -1645,7 +1645,7 @@ describe("BondingV5", function () { await bondingConfig.connect(owner).setAcpSkillLauncher(user1.address, true); }); - it("Should revert Pegasus X_LAUNCH with non-zero airdropPercent", async function () { + it("Should revert X_LAUNCH with non-zero airdropPercent", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1656,7 +1656,7 @@ describe("BondingV5", function () { await expect( bondingV5.connect(user1).preLaunch( - "Invalid Pegasus", "INV", [0, 1], "Description", + "Invalid X_LAUNCH", "INV", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, 5, false, ANTI_SNIPER_NONE, false @@ -1664,7 +1664,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); }); - it("Should revert Pegasus X_LAUNCH with needAcf = true", async function () { + it("Should revert X_LAUNCH with needAcf = true", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1675,7 +1675,7 @@ describe("BondingV5", function () { await expect( bondingV5.connect(user1).preLaunch( - "Invalid Pegasus ACF", "INVA", [0, 1], "Description", + "Invalid X_LAUNCH ACF", "INVA", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, 0, true, ANTI_SNIPER_NONE, false @@ -1683,7 +1683,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); }); - it("Should revert Pegasus X_LAUNCH with non-NONE anti-sniper type", async function () { + it("Should revert X_LAUNCH with non-NONE anti-sniper type", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1694,7 +1694,7 @@ describe("BondingV5", function () { await expect( bondingV5.connect(user1).preLaunch( - "Invalid Pegasus AS", "INAS", [0, 1], "Description", + "Invalid X_LAUNCH AS", "INAS", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, 0, false, ANTI_SNIPER_60S, false @@ -1702,7 +1702,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); }); - it("Should revert Pegasus X_LAUNCH with isProject60days = true", async function () { + it("Should revert X_LAUNCH with isProject60days = true", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1713,7 +1713,7 @@ describe("BondingV5", function () { await expect( bondingV5.connect(user1).preLaunch( - "Invalid Pegasus 60D", "IN60", [0, 1], "Description", + "Invalid X_LAUNCH 60D", "IN60", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, 0, false, ANTI_SNIPER_NONE, true @@ -1721,7 +1721,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); }); - it("Should revert Pegasus X_LAUNCH with scheduled launch (not immediate)", async function () { + it("Should revert X_LAUNCH with scheduled launch (not immediate)", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1733,7 +1733,7 @@ describe("BondingV5", function () { await expect( bondingV5.connect(user1).preLaunch( - "Scheduled Pegasus", "SCHP", [0, 1], "Description", + "Scheduled X_LAUNCH", "SCHP", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, 0, false, ANTI_SNIPER_NONE, false @@ -1741,7 +1741,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); }); - it("Should revert Pegasus ACP_SKILL with any invalid param combination", async function () { + it("Should revert ACP_SKILL with non-zero airdropPercent", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1750,16 +1750,92 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + 100; - // Test all invalid params for ACP_SKILL await expect( bondingV5.connect(user1).preLaunch( - "Invalid ACP", "IACP", [0, 1], "Description", + "Invalid ACP_SKILL", "IACP", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, LAUNCH_MODE_ACP_SKILL, 5, false, ANTI_SNIPER_NONE, false ) ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); }); + + it("Should revert ACP_SKILL with needAcf = true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid ACP_SKILL ACF", "IACF", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_ACP_SKILL, 0, true, ANTI_SNIPER_NONE, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert ACP_SKILL with non-NONE anti-sniper type", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid ACP_SKILL AS", "IAAS", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_ACP_SKILL, 0, false, ANTI_SNIPER_98M, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert ACP_SKILL with isProject60days = true", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + 100; + + await expect( + bondingV5.connect(user1).preLaunch( + "Invalid ACP_SKILL 60D", "IA60", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_ACP_SKILL, 0, false, ANTI_SNIPER_NONE, true + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); + + it("Should revert ACP_SKILL with scheduled launch (not immediate)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + // Scheduled launch (startTime >= now + 24h) + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + await expect( + bondingV5.connect(user1).preLaunch( + "Scheduled ACP_SKILL", "SACP", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_ACP_SKILL, 0, false, ANTI_SNIPER_NONE, false + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); + }); }); // ============================================ @@ -2271,4 +2347,525 @@ describe("BondingV5", function () { expect(launchParams.isProject60days).to.equal(true); }); }); + + // ============================================ + // cancelLaunch Tests + // ============================================ + describe("cancelLaunch", function () { + let tokenAddress; + let pairAddress; + + beforeEach(async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "Cancel Test Token", + "CTT", + [0, 1, 2], + "Test token for cancel", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + const parsedEvent = bondingV5.interface.parseLog(event); + tokenAddress = parsedEvent.args.token; + pairAddress = parsedEvent.args.pair; + }); + + it("Should allow creator to cancel launch before launch() is called", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const balanceBefore = await virtualToken.balanceOf(user1.address); + + const tx = await bondingV5.connect(user1).cancelLaunch(tokenAddress); + const receipt = await tx.wait(); + + // Verify CancelledLaunch event + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "CancelledLaunch"; + } catch (e) { + return false; + } + }); + expect(event).to.not.be.undefined; + + // Verify initialPurchase is returned to creator + const balanceAfter = await virtualToken.balanceOf(user1.address); + expect(balanceAfter).to.be.greaterThan(balanceBefore); + + // Verify token status is updated + const tokenInfo = await bondingV5.tokenInfo(tokenAddress); + expect(tokenInfo.launchExecuted).to.be.true; + expect(tokenInfo.initialPurchase).to.equal(0); + }); + + it("Should revert if non-creator tries to cancel", async function () { + const { user2 } = accounts; + const { bondingV5 } = contracts; + + await expect( + bondingV5.connect(user2).cancelLaunch(tokenAddress) + ).to.be.revertedWithCustomError(bondingV5, "InvalidInput"); + }); + + it("Should revert if trying to cancel after launch() is called", async function () { + const { user1 } = accounts; + const { bondingV5 } = contracts; + + // Wait and launch first + await time.increase(START_TIME_DELAY + 1); + await bondingV5.launch(tokenAddress); + + // Then try to cancel + await expect( + bondingV5.connect(user1).cancelLaunch(tokenAddress) + ).to.be.revertedWithCustomError(bondingV5, "InvalidTokenStatus"); + }); + + it("Should revert if trying to cancel twice", async function () { + const { user1 } = accounts; + const { bondingV5 } = contracts; + + // First cancel + await bondingV5.connect(user1).cancelLaunch(tokenAddress); + + // Second cancel should fail + await expect( + bondingV5.connect(user1).cancelLaunch(tokenAddress) + ).to.be.revertedWithCustomError(bondingV5, "InvalidTokenStatus"); + }); + + it("Should revert when cancelling non-existent token", async function () { + const { user1 } = accounts; + const { bondingV5 } = contracts; + + await expect( + bondingV5.connect(user1).cancelLaunch(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(bondingV5, "InvalidInput"); + }); + }); + + // ============================================ + // BondingV5 Admin Functions Tests + // ============================================ + describe("BondingV5 Admin Functions", function () { + it("Should allow owner to update BondingConfig", async function () { + const { owner } = accounts; + const { bondingV5, bondingConfig } = contracts; + + // Deploy a new BondingConfig for testing + const BondingConfig = await ethers.getContractFactory("BondingConfig"); + const newBondingConfig = await upgrades.deployProxy( + BondingConfig, + [ + INITIAL_SUPPLY, + owner.address, + owner.address, + MAX_AIRDROP_PERCENT, + { startTimeDelay: START_TIME_DELAY, normalLaunchFee: NORMAL_LAUNCH_FEE, acfFee: ACF_FEE }, + { tbaSalt: TBA_SALT, tbaImplementation: TBA_IMPLEMENTATION, daoVotingPeriod: DAO_VOTING_PERIOD, daoThreshold: DAO_THRESHOLD }, + { fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ, targetRealVirtual: TARGET_REAL_VIRTUAL }, + ], + { initializer: "initialize" } + ); + await newBondingConfig.waitForDeployment(); + + // Update BondingConfig + await bondingV5.connect(owner).setBondingConfig(await newBondingConfig.getAddress()); + + // Verify the update + expect(await bondingV5.bondingConfig()).to.equal(await newBondingConfig.getAddress()); + + // Reset to original + await bondingV5.connect(owner).setBondingConfig(await bondingConfig.getAddress()); + }); + + it("Should revert if non-owner tries to update BondingConfig", async function () { + const { user1 } = accounts; + const { bondingV5 } = contracts; + + await expect( + bondingV5.connect(user1).setBondingConfig(ethers.ZeroAddress) + ).to.be.revertedWithCustomError(bondingV5, "OwnableUnauthorizedAccount"); + }); + }); + + // ============================================ + // View Functions Tests + // ============================================ + describe("View Functions", function () { + let tokenAddress; + + beforeEach(async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "View Test Token", + "VTT", + [0, 1, 2], + "Test token for view functions", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 3, + true, + ANTI_SNIPER_98M, + true + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + + tokenAddress = bondingV5.interface.parseLog(event).args.token; + }); + + it("Should return correct isProject60days value", async function () { + const { bondingV5 } = contracts; + expect(await bondingV5.isProject60days(tokenAddress)).to.be.true; + }); + + it("Should return correct isProjectXLaunch value (false for NORMAL mode)", async function () { + const { bondingV5 } = contracts; + expect(await bondingV5.isProjectXLaunch(tokenAddress)).to.be.false; + }); + + it("Should return correct isAcpSkillLaunch value (false for NORMAL mode)", async function () { + const { bondingV5 } = contracts; + expect(await bondingV5.isAcpSkillLaunch(tokenAddress)).to.be.false; + }); + + it("Should return correct tokenAntiSniperType value", async function () { + const { bondingV5 } = contracts; + expect(await bondingV5.tokenAntiSniperType(tokenAddress)).to.equal(ANTI_SNIPER_98M); + }); + + it("Should revert tokenAntiSniperType for non-BondingV5 token", async function () { + const { bondingV5 } = contracts; + + // Use a random address that doesn't exist as a token + const randomAddress = ethers.Wallet.createRandom().address; + + await expect( + bondingV5.tokenAntiSniperType(randomAddress) + ).to.be.revertedWithCustomError(bondingV5, "InvalidTokenStatus"); + }); + + it("Should return correct tokenGradThreshold value", async function () { + const { bondingV5 } = contracts; + const gradThreshold = await bondingV5.tokenGradThreshold(tokenAddress); + expect(gradThreshold).to.be.greaterThan(0); + }); + + it("Should return correct tokenInfo values", async function () { + const { user1 } = accounts; + const { bondingV5 } = contracts; + + const tokenInfo = await bondingV5.tokenInfo(tokenAddress); + + expect(tokenInfo.creator).to.equal(user1.address); + expect(tokenInfo.token).to.equal(tokenAddress); + expect(tokenInfo.pair).to.not.equal(ethers.ZeroAddress); + expect(tokenInfo.trading).to.be.true; + expect(tokenInfo.tradingOnUniswap).to.be.false; + expect(tokenInfo.launchExecuted).to.be.false; + }); + + it("Should return correct tokenLaunchParams values", async function () { + const { bondingV5 } = contracts; + + const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); + + expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); + expect(launchParams.airdropPercent).to.equal(3); + expect(launchParams.needAcf).to.be.true; + expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); + expect(launchParams.isProject60days).to.be.true; + }); + }); + + // ============================================ + // BondingConfig Additional Tests + // ============================================ + describe("BondingConfig Additional Functions", function () { + it("Should correctly calculate bonding curve supply for various scenarios", async function () { + const { bondingConfig } = contracts; + + const initialSupply = BigInt(INITIAL_SUPPLY); + + // 0% airdrop, no ACF: 100% bonding curve + const supply100 = await bondingConfig.calculateBondingCurveSupply(0, false); + expect(supply100).to.equal(initialSupply); + + // 5% airdrop, no ACF: 95% bonding curve + const supply95 = await bondingConfig.calculateBondingCurveSupply(5, false); + expect(supply95).to.equal((initialSupply * 95n) / 100n); + + // 0% airdrop, with ACF: 50% bonding curve + const supply50 = await bondingConfig.calculateBondingCurveSupply(0, true); + expect(supply50).to.equal((initialSupply * 50n) / 100n); + + // 4% airdrop, with ACF: 46% bonding curve + const supply46 = await bondingConfig.calculateBondingCurveSupply(4, true); + expect(supply46).to.equal((initialSupply * 46n) / 100n); + }); + + it("Should correctly identify special modes", async function () { + const { bondingConfig } = contracts; + + expect(await bondingConfig.isSpecialMode(LAUNCH_MODE_NORMAL)).to.be.false; + expect(await bondingConfig.isSpecialMode(LAUNCH_MODE_X_LAUNCH)).to.be.true; + expect(await bondingConfig.isSpecialMode(LAUNCH_MODE_ACP_SKILL)).to.be.true; + }); + + it("Should return correct fakeInitialVirtualLiq", async function () { + const { bondingConfig } = contracts; + expect(await bondingConfig.getFakeInitialVirtualLiq()).to.equal(FAKE_INITIAL_VIRTUAL_LIQ); + }); + + it("Should return correct targetRealVirtual", async function () { + const { bondingConfig } = contracts; + expect(await bondingConfig.getTargetRealVirtual()).to.equal(TARGET_REAL_VIRTUAL); + }); + + it("Should correctly calculate launch fee for different scenarios", async function () { + const { bondingConfig } = contracts; + + // Immediate launch, no ACF: 0 + expect(await bondingConfig.calculateLaunchFee(false, false)).to.equal(0); + + // Immediate launch, with ACF: acfFee + expect(await bondingConfig.calculateLaunchFee(false, true)).to.equal(ACF_FEE); + + // Scheduled launch, no ACF: normalLaunchFee + expect(await bondingConfig.calculateLaunchFee(true, false)).to.equal(NORMAL_LAUNCH_FEE); + + // Scheduled launch, with ACF: normalLaunchFee + acfFee + expect(await bondingConfig.calculateLaunchFee(true, true)).to.equal(NORMAL_LAUNCH_FEE + ACF_FEE); + }); + + it("Should allow owner to set deploy params", async function () { + const { owner } = accounts; + const { bondingConfig } = contracts; + + const newDeployParams = { + tbaSalt: ethers.keccak256(ethers.toUtf8Bytes("new_salt")), + tbaImplementation: ethers.Wallet.createRandom().address, + daoVotingPeriod: 7200, + daoThreshold: ethers.parseEther("200"), + }; + + await bondingConfig.connect(owner).setDeployParams(newDeployParams); + + const deployParams = await bondingConfig.deployParams(); + expect(deployParams.tbaSalt).to.equal(newDeployParams.tbaSalt); + expect(deployParams.tbaImplementation).to.equal(newDeployParams.tbaImplementation); + expect(deployParams.daoVotingPeriod).to.equal(newDeployParams.daoVotingPeriod); + expect(deployParams.daoThreshold).to.equal(newDeployParams.daoThreshold); + + // Reset to original + await bondingConfig.connect(owner).setDeployParams({ + tbaSalt: TBA_SALT, + tbaImplementation: TBA_IMPLEMENTATION, + daoVotingPeriod: DAO_VOTING_PERIOD, + daoThreshold: DAO_THRESHOLD, + }); + }); + + it("Should allow owner to set common params", async function () { + const { owner } = accounts; + const { bondingConfig } = contracts; + + const newSupply = ethers.parseUnits("2000000000", 0); // 2B base units + const newFeeTo = ethers.Wallet.createRandom().address; + + await bondingConfig.connect(owner).setCommonParams(newSupply, newFeeTo); + + expect(await bondingConfig.initialSupply()).to.equal(newSupply); + expect(await bondingConfig.feeTo()).to.equal(newFeeTo); + + // Reset to original + await bondingConfig.connect(owner).setCommonParams(INITIAL_SUPPLY, owner.address); + }); + + it("Should allow owner to set team token reserved wallet", async function () { + const { owner, user2 } = accounts; + const { bondingConfig } = contracts; + + const originalWallet = await bondingConfig.teamTokenReservedWallet(); + + await bondingConfig.connect(owner).setTeamTokenReservedWallet(user2.address); + expect(await bondingConfig.teamTokenReservedWallet()).to.equal(user2.address); + + // Reset to original + await bondingConfig.connect(owner).setTeamTokenReservedWallet(originalWallet); + }); + }); + + // ============================================ + // Fee Collection Tests + // ============================================ + describe("Fee Collection", function () { + it("Should collect correct fee for scheduled launch with ACF", async function () { + const { user1, owner } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const feeToBalanceBefore = await virtualToken.balanceOf(owner.address); + + // Scheduled launch with ACF + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + await bondingV5.connect(user1).preLaunch( + "Fee Test Token", "FTT", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, true, ANTI_SNIPER_60S, false + ); + + const feeToBalanceAfter = await virtualToken.balanceOf(owner.address); + const feeCollected = feeToBalanceAfter - feeToBalanceBefore; + + // Expected fee: normalLaunchFee + acfFee + expect(feeCollected).to.equal(NORMAL_LAUNCH_FEE + ACF_FEE); + }); + + it("Should not collect fee for immediate launch without ACF", async function () { + const { user1, owner } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const feeToBalanceBefore = await virtualToken.balanceOf(owner.address); + + // Immediate launch without ACF + const startTime = (await time.latest()) + 100; + await bondingV5.connect(user1).preLaunch( + "No Fee Token", "NFT", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 0, false, ANTI_SNIPER_60S, false + ); + + const feeToBalanceAfter = await virtualToken.balanceOf(owner.address); + const feeCollected = feeToBalanceAfter - feeToBalanceBefore; + + expect(feeCollected).to.equal(0); + }); + }); + + // ============================================ + // Token Reserved Transfer Tests + // ============================================ + describe("Token Reserved Transfer", function () { + it("Should transfer reserved tokens to teamTokenReservedWallet", async function () { + const { user1, beOpsWallet } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const reservedWalletBalanceBefore = await ethers.provider.getBalance(beOpsWallet.address); + + // Create token with 5% airdrop (should transfer 5% to reserved wallet) + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "Reserved Test Token", "RTT", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + // Check that reserved tokens were transferred to teamTokenReservedWallet + const actualTokenContract = await ethers.getContractAt("AgentTokenV2", tokenAddress); + const reservedWalletTokenBalance = await actualTokenContract.balanceOf(beOpsWallet.address); + + // 5% of 1B = 50M tokens (with 18 decimals) + const expectedReserved = ethers.parseEther("50000000"); + expect(reservedWalletTokenBalance).to.equal(expectedReserved); + }); + + it("Should transfer 50% + airdrop tokens when needAcf is true", async function () { + const { user1, beOpsWallet } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + // Create token with 4% airdrop and needAcf = true (54% total reserved) + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5.connect(user1).preLaunch( + "ACF Reserved Test", "ART", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { return bondingV5.interface.parseLog(log)?.name === "PreLaunched"; } + catch (e) { return false; } + }); + const tokenAddress = bondingV5.interface.parseLog(event).args.token; + + // Check reserved tokens (54% = 4% airdrop + 50% ACF) + const actualTokenContract = await ethers.getContractAt("AgentTokenV2", tokenAddress); + const reservedWalletTokenBalance = await actualTokenContract.balanceOf(beOpsWallet.address); + + // 54% of 1B = 540M tokens (with 18 decimals) + const expectedReserved = ethers.parseEther("540000000"); + expect(reservedWalletTokenBalance).to.equal(expectedReserved); + }); + }); }); From f61d1ee7efeb9aca938af368824ad406fa1ddbb9 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Wed, 11 Mar 2026 12:04:55 +0800 Subject: [PATCH 3/8] grant roles and optimize script logs --- scripts/launchpadv5/deployLaunchpadv5_0.ts | 24 +++- scripts/launchpadv5/deployLaunchpadv5_1.ts | 16 +-- scripts/launchpadv5/deployLaunchpadv5_2.ts | 55 ++++--- scripts/launchpadv5/deployLaunchpadv5_3.ts | 129 +++++++---------- scripts/launchpadv5/deployLaunchpadv5_4.ts | 160 ++++++++------------- scripts/launchpadv5/handle_agent_tax.ts | 11 +- 6 files changed, 177 insertions(+), 218 deletions(-) diff --git a/scripts/launchpadv5/deployLaunchpadv5_0.ts b/scripts/launchpadv5/deployLaunchpadv5_0.ts index 321f5b5..7338381 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_0.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_0.ts @@ -121,14 +121,26 @@ const { ethers, upgrades } = require("hardhat"); ); await agentNftV2.waitForDeployment(); agentNftV2Address = await agentNftV2.getAddress(); - console.log("AgentNftV2 (proxy) deployed at:", agentNftV2Address); + console.log("AgentNftV2 deployed at:", agentNftV2Address); deployedContracts["AGENT_NFT_V2_ADDRESS"] = agentNftV2Address!; // Grant DEFAULT_ADMIN_ROLE to admin as well const defaultAdminRole = await agentNftV2.DEFAULT_ADMIN_ROLE(); const tx = await agentNftV2.grantRole(defaultAdminRole, admin); await tx.wait(); - console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); + console.log("DEFAULT_ADMIN_ROLE of AgentNftV2 granted to admin:", admin); + + // Grant ADMIN_ROLE to admin as well + // because agentVeTokenV2.setMatureAt() needs agentNftV2.adminRole + const adminRole = await agentNftV2.ADMIN_ROLE(); + const tx2 = await agentNftV2.grantRole(adminRole, admin); + await tx2.wait(); + console.log("ADMIN_ROLE of AgentNftV2 granted to admin:", admin); + + const validatorAdminRole = await agentNftV2.VALIDATOR_ADMIN_ROLE(); + const tx3 = await agentNftV2.grantRole(validatorAdminRole, admin); + await tx3.wait(); + console.log("VALIDATOR_ADMIN_ROLE of AgentNftV2 granted to admin:", admin); // Verify AgentNftV2 proxy await verifyContract(agentNftV2Address!); @@ -169,20 +181,20 @@ const { ethers, upgrades } = require("hardhat"); console.log("AgentTax deployed at:", agentTaxAddress); deployedContracts["AGENT_TOKEN_TAX_MANAGER"] = agentTaxAddress; - // Grant EXECUTOR_V2_ROLE to all BE_TAX_OPS_WALLETS + // Grant EXECUTOR_V2_ROLE to BE_TAX_OPS_WALLETS (for updateCreator functions) const executorV2Role = await agentTax.EXECUTOR_V2_ROLE(); for (const wallet of beTaxOpsWalletList) { const grantTx = await agentTax.grantRole(executorV2Role, wallet); await grantTx.wait(); - console.log("EXECUTOR_V2_ROLE granted to:", wallet); + console.log("EXECUTOR_V2_ROLE of AgentTax granted to:", wallet); } - // Grant EXECUTOR_ROLE to all BE_HANDLE_AGENT_TAXES_WALLETS + // Grant EXECUTOR_ROLE to BE_HANDLE_AGENT_TAXES_WALLETS (for handleAgentTaxes) const executorRole = await agentTax.EXECUTOR_ROLE(); for (const wallet of beHandleAgentTaxesWalletList) { const grantTx = await agentTax.grantRole(executorRole, wallet); await grantTx.wait(); - console.log("EXECUTOR_ROLE granted to:", wallet); + console.log("EXECUTOR_ROLE of AgentTax granted to:", wallet); } // Verify AgentTax proxy diff --git a/scripts/launchpadv5/deployLaunchpadv5_1.ts b/scripts/launchpadv5/deployLaunchpadv5_1.ts index 05bc1bb..297b0da 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_1.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_1.ts @@ -169,24 +169,24 @@ const { ethers, upgrades } = require("hardhat"); const adminRole = await fFactoryV2.ADMIN_ROLE(); const defaultAdminRole = await fFactoryV2.DEFAULT_ADMIN_ROLE(); - // Grant ADMIN_ROLE to deployer temporarily + // Grant ADMIN_ROLE to deployer temporarily (needed for setRouter) const tx1 = await fFactoryV2.grantRole(adminRole, deployerAddress); await tx1.wait(); - console.log("ADMIN_ROLE granted to deployer temporarily"); + console.log("ADMIN_ROLE of FFactoryV2 granted to deployer (temporary)"); // Set Router const tx2 = await fFactoryV2.setRouter(fRouterV2Address); await tx2.wait(); - console.log("Router set in FFactoryV2"); + console.log("FFactoryV2.setRouter() called with:", fRouterV2Address); // Grant roles to admin const tx3 = await fFactoryV2.grantRole(adminRole, admin); await tx3.wait(); - console.log("ADMIN_ROLE granted to admin:", admin); + console.log("ADMIN_ROLE of FFactoryV2 granted to admin:", admin); const tx4 = await fFactoryV2.grantRole(defaultAdminRole, admin); await tx4.wait(); - console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); + console.log("DEFAULT_ADMIN_ROLE of FFactoryV2 granted to admin:", admin); } // ============================================ @@ -198,18 +198,18 @@ const { ethers, upgrades } = require("hardhat"); // Grant ADMIN_ROLE to admin (needed for setBondingV5) const tx5 = await fRouterV2.grantRole(await fRouterV2.ADMIN_ROLE(), admin); await tx5.wait(); - console.log("ADMIN_ROLE granted to admin on FRouterV2"); + console.log("ADMIN_ROLE of FRouterV2 granted to admin:", admin); // Grant DEFAULT_ADMIN_ROLE to admin const tx6 = await fRouterV2.grantRole(await fRouterV2.DEFAULT_ADMIN_ROLE(), admin); await tx6.wait(); - console.log("DEFAULT_ADMIN_ROLE granted to admin on FRouterV2"); + console.log("DEFAULT_ADMIN_ROLE of FRouterV2 granted to admin:", admin); // Grant EXECUTOR_ROLE to BE_OPS_WALLET (for resetTime) const executorRole = await fRouterV2.EXECUTOR_ROLE(); const tx7 = await fRouterV2.grantRole(executorRole, beOpsWallet); await tx7.wait(); - console.log("EXECUTOR_ROLE granted to BE_OPS_WALLET:", beOpsWallet); + console.log("EXECUTOR_ROLE of FRouterV2 granted to BE_OPS_WALLET:", beOpsWallet); } // NOTE: Deployer roles are NOT revoked here diff --git a/scripts/launchpadv5/deployLaunchpadv5_2.ts b/scripts/launchpadv5/deployLaunchpadv5_2.ts index afa7631..4dcf954 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_2.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_2.ts @@ -271,55 +271,64 @@ const { ethers, upgrades } = require("hardhat"); // ============================================ console.log("\n--- Configuring AgentFactoryV6 ---"); - // Grant DEFAULT_ADMIN_ROLE to deployer temporarily + // Grant DEFAULT_ADMIN_ROLE to deployer temporarily (needed for setParams/setTokenParams) const txGrantAdmin = await agentFactoryV6.grantRole( await agentFactoryV6.DEFAULT_ADMIN_ROLE(), deployerAddress ); await txGrantAdmin.wait(); - console.log("DEFAULT_ADMIN_ROLE granted to deployer temporarily"); + console.log("DEFAULT_ADMIN_ROLE of AgentFactoryV6 granted to deployer (temporary)"); // Set params const txSetParams = await agentFactoryV6.setParams( - agentFactoryV6MaturityDuration, // maturity duration (from env) + agentFactoryV6MaturityDuration, uniswapV2RouterAddress, admin, // defaultDelegatee admin // tokenAdmin ); await txSetParams.wait(); - console.log( - "setParams() called for AgentFactoryV6 with maturityDuration:", - agentFactoryV6MaturityDuration - ); + console.log("AgentFactoryV6.setParams() called:", { + maturityDuration: agentFactoryV6MaturityDuration, + uniswapRouter: uniswapV2RouterAddress, + defaultDelegatee: admin, + tokenAdmin: admin, + }); // Set token params (Sentient phase tax configuration) const txSetTokenParams = await agentFactoryV6.setTokenParams( - sentientBuyTax, // projectBuyTaxBasisPoints - sentientSellTax, // projectSellTaxBasisPoints - taxSwapThresholdBasisPoints, // taxSwapThresholdBasisPoints (from env) - agentTokenTaxManager // projectTaxRecipient (fee address) - ); - await txSetTokenParams.wait(); - console.log("setTokenParams() called for AgentFactoryV6:", { sentientBuyTax, sentientSellTax, taxSwapThresholdBasisPoints, - agentTokenTaxManager, + agentTokenTaxManager + ); + await txSetTokenParams.wait(); + console.log("AgentFactoryV6.setTokenParams() called:", { + buyTax: sentientBuyTax, + sellTax: sentientSellTax, + taxSwapThreshold: taxSwapThresholdBasisPoints, + taxRecipient: agentTokenTaxManager, }); - // Grant DEFAULT_ADMIN_ROLE to admin + // Grant DEFAULT_ADMIN_ROLE of AgentFactoryV6 to admin + const agentFactoryV6DefaultAdminRole = await agentFactoryV6.DEFAULT_ADMIN_ROLE(); const txGrantAdminToAdmin = await agentFactoryV6.grantRole( - await agentFactoryV6.DEFAULT_ADMIN_ROLE(), + agentFactoryV6DefaultAdminRole, admin ); await txGrantAdminToAdmin.wait(); - console.log("DEFAULT_ADMIN_ROLE granted to admin:", admin); + console.log("DEFAULT_ADMIN_ROLE of AgentFactoryV6 granted to admin:", admin); - // Grant REMOVE_LIQUIDITY_ROLE to admin - const removeLiqRole = await agentFactoryV6.REMOVE_LIQUIDITY_ROLE(); - const txGrantRemoveLiq = await agentFactoryV6.grantRole(removeLiqRole, admin); + // Grant REMOVE_LIQUIDITY_ROLE of AgentFactoryV6 to admin + const agentFactoryV6RemoveLiqRole = await agentFactoryV6.REMOVE_LIQUIDITY_ROLE(); + const txGrantRemoveLiq = await agentFactoryV6.grantRole(agentFactoryV6RemoveLiqRole, admin); await txGrantRemoveLiq.wait(); - console.log("REMOVE_LIQUIDITY_ROLE granted to admin:", admin); + console.log("REMOVE_LIQUIDITY_ROLE of AgentFactoryV6 granted to admin:", admin); + + // Grant WITHDRAW_ROLE of AgentFactoryV6 to admin + const agentFactoryV6WithdrawRole = await agentFactoryV6.WITHDRAW_ROLE(); + const txGrantWithdraw = await agentFactoryV6.grantRole(agentFactoryV6WithdrawRole, admin); + await txGrantWithdraw.wait(); + console.log("WITHDRAW_ROLE of AgentFactoryV6 granted to admin:", admin); // ============================================ // 9. Grant MINTER_ROLE on AgentNftV2 to AgentFactoryV6 @@ -340,7 +349,7 @@ const { ethers, upgrades } = require("hardhat"); const minterRole = await agentNftV2Contract.MINTER_ROLE(); const tx = await agentNftV2Contract.grantRole(minterRole, agentFactoryV6Address); await tx.wait(); - console.log("MINTER_ROLE granted to AgentFactoryV6:", agentFactoryV6Address); + console.log("MINTER_ROLE of AgentNftV2 granted to AgentFactoryV6:", agentFactoryV6Address); } else { console.log( "\n⚠️ Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentNftV2" diff --git a/scripts/launchpadv5/deployLaunchpadv5_3.ts b/scripts/launchpadv5/deployLaunchpadv5_3.ts index 8fe8c19..6623e61 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_3.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_3.ts @@ -86,7 +86,15 @@ const { ethers, upgrades } = require("hardhat"); if (!targetRealVirtual) { throw new Error("TARGET_REAL_VIRTUAL not set in environment"); } - const maxAirdropPercent = process.env.MAX_AIRDROP_PERCENT || "5"; + const agentTaxAddress = process.env.AGENT_TOKEN_TAX_MANAGER; + if (!agentTaxAddress) { + throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + } + + const maxAirdropPercent = process.env.MAX_AIRDROP_PERCENT; + if (!maxAirdropPercent) { + throw new Error("MAX_AIRDROP_PERCENT not set in environment"); + } console.log("\nDeployment arguments loaded:", { contractController, @@ -155,27 +163,27 @@ const { ethers, upgrades } = require("hardhat"); // Verify BondingConfig proxy await verifyContract(bondingConfigAddress); - // Set X Launchers + // Set X Launchers (authorized addresses for LAUNCH_MODE_X_LAUNCH) const xLauncherAddresses = process.env.X_LAUNCHER_ADDRESSES; if (xLauncherAddresses) { const addresses = xLauncherAddresses.split(",").map((addr) => addr.trim()).filter((addr) => addr); - console.log("\n--- Setting X Launchers ---"); + console.log("\n--- Setting X Launchers in BondingConfig ---"); for (const addr of addresses) { const tx = await bondingConfig.setXLauncher(addr, true); await tx.wait(); - console.log(`Set X Launcher: ${addr}`); + console.log("BondingConfig.setXLauncher(true):", addr); } } - // Set ACP Skill Launchers + // Set ACP Skill Launchers (authorized addresses for LAUNCH_MODE_ACP_SKILL) const acpSkillLauncherAddresses = process.env.ACP_SKILL_LAUNCHER_ADDRESSES; if (acpSkillLauncherAddresses) { const addresses = acpSkillLauncherAddresses.split(",").map((addr) => addr.trim()).filter((addr) => addr); - console.log("\n--- Setting ACP Skill Launchers ---"); + console.log("\n--- Setting ACP Skill Launchers in BondingConfig ---"); for (const addr of addresses) { const tx = await bondingConfig.setAcpSkillLauncher(addr, true); await tx.wait(); - console.log(`Set ACP Skill Launcher: ${addr}`); + console.log("BondingConfig.setAcpSkillLauncher(true):", addr); } } @@ -207,86 +215,52 @@ const { ethers, upgrades } = require("hardhat"); // ============================================ // 3. Transfer Ownership // ============================================ - console.log("\n--- Transferring ownership to CONTRACT_CONTROLLER ---"); + console.log("\n--- Transferring ownership ---"); const tx5 = await bondingV5.transferOwnership(contractController); await tx5.wait(); - console.log("BondingV5 ownership transferred to CONTRACT_CONTROLLER"); + console.log("BondingV5 ownership transferred to CONTRACT_CONTROLLER:", contractController); const tx6 = await bondingConfig.transferOwnership(contractController); await tx6.wait(); - console.log("BondingConfig ownership transferred to CONTRACT_CONTROLLER"); + console.log("BondingConfig ownership transferred to CONTRACT_CONTROLLER:", contractController); // ============================================ - // 4. Grant Roles and Configure Contracts (using admin wallet) + // 4. Grant Roles and Configure Contracts // ============================================ - // These contracts were deployed in previous scripts, so deployer no longer has admin roles - // We need to use ADMIN_PRIVATE_KEY to grant roles - console.log("\n--- Granting necessary roles (using admin wallet) ---"); + // Deployer still has admin roles from previous scripts (_0, _1, _2) + // Roles will be revoked in _4.ts + console.log("\n--- Granting roles and configuring contracts ---"); - const adminPrivateKey = process.env.ADMIN_PRIVATE_KEY; - const agentTaxAddress = process.env.AGENT_TOKEN_TAX_MANAGER; - if (!agentTaxAddress) { - throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); - } - if (!adminPrivateKey) { - console.log("\n" + "=".repeat(80)); - console.log("⚠️ ADMIN_PRIVATE_KEY not set - Manual role grants required!"); - console.log("⚠️ The following operations must be done manually by admin:"); - console.log("=".repeat(80)); - console.log(`1. FFactoryV2.grantRole(CREATOR_ROLE, ${bondingV5Address})`); - console.log(`2. FRouterV2.setBondingV5(${bondingV5Address}, ${bondingConfigAddress})`); - console.log(`3. FRouterV2.grantRole(EXECUTOR_ROLE, ${bondingV5Address})`); - console.log(`4. AgentFactoryV6.grantRole(BONDING_ROLE, ${bondingV5Address})`); - console.log(`5. AgentTax.setBondingV5(${bondingV5Address})`); - console.log("=".repeat(80)); - throw new Error("ADMIN_PRIVATE_KEY not set - Manual role grants required!"); - } else { - const adminSigner = new ethers.Wallet(adminPrivateKey, ethers.provider); - console.log("Using admin signer:", await adminSigner.getAddress()); - - const fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address, adminSigner); - const fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address, adminSigner); - const agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address, adminSigner); - - // 3.1 Grant CREATOR_ROLE of FFactoryV2 to BondingV5 - console.log("\n--- Granting CREATOR_ROLE of FFactoryV2 to BondingV5 ---"); - const creatorRole = await fFactoryV2.CREATOR_ROLE(); - await (await fFactoryV2.grantRole(creatorRole, bondingV5Address)).wait(); - console.log("✅ Granted CREATOR_ROLE of FFactoryV2 to BondingV5"); - - // 3.2 Set BondingV5 and BondingConfig in FRouterV2 - console.log("\n--- Setting BondingV5 in FRouterV2 ---"); - await (await fRouterV2.setBondingV5(bondingV5Address, bondingConfigAddress)).wait(); - console.log("✅ Set BondingV5 and BondingConfig in FRouterV2"); - - // 3.3 Grant EXECUTOR_ROLE of FRouterV2 to BondingV5 - console.log("\n--- Granting EXECUTOR_ROLE of FRouterV2 to BondingV5 ---"); - const executorRole = await fRouterV2.EXECUTOR_ROLE(); - await (await fRouterV2.grantRole(executorRole, bondingV5Address)).wait(); - console.log("✅ Granted EXECUTOR_ROLE of FRouterV2 to BondingV5"); - - // 3.4 Grant BONDING_ROLE of AgentFactoryV6 to BondingV5 - console.log("\n--- Granting BONDING_ROLE of AgentFactoryV6 to BondingV5 ---"); - const bondingRole = await agentFactoryV6.BONDING_ROLE(); - await (await agentFactoryV6.grantRole(bondingRole, bondingV5Address)).wait(); - console.log("✅ Granted BONDING_ROLE of AgentFactoryV6 to BondingV5"); - - // 3.5 Set BondingV5 in AgentTax (optional - only if AGENT_TAX_ADDRESS is set) - const agentTaxAddress = process.env.AGENT_TAX_ADDRESS; - if (agentTaxAddress) { - console.log("\n--- Setting BondingV5 in AgentTax ---"); - const agentTax = await ethers.getContractAt("AgentTax", agentTaxAddress, adminSigner); - await (await agentTax.setBondingV5(bondingV5Address)).wait(); - console.log("✅ Set BondingV5 in AgentTax:", bondingV5Address); - } else { - console.log("\n⚠️ AGENT_TAX_ADDRESS not set - skipping AgentTax.setBondingV5()"); - console.log(" If you have AgentTax deployed, run manually:"); - console.log(` agentTax.setBondingV5(${bondingV5Address})`); - } + const fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address); + const fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address); + const agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address); + const agentTax = await ethers.getContractAt("AgentTax", agentTaxAddress); - console.log("\n✅ All role grants completed!"); - } + // Grant CREATOR_ROLE of FFactoryV2 to BondingV5 + const creatorRole = await fFactoryV2.CREATOR_ROLE(); + await (await fFactoryV2.grantRole(creatorRole, bondingV5Address)).wait(); + console.log("✅ CREATOR_ROLE of FFactoryV2 granted to BondingV5:", bondingV5Address); + + // Set BondingV5 and BondingConfig in FRouterV2 + await (await fRouterV2.setBondingV5(bondingV5Address, bondingConfigAddress)).wait(); + console.log("✅ FRouterV2.setBondingV5() called:", { bondingV5: bondingV5Address, bondingConfig: bondingConfigAddress }); + + // Grant EXECUTOR_ROLE of FRouterV2 to BondingV5 + const executorRole = await fRouterV2.EXECUTOR_ROLE(); + await (await fRouterV2.grantRole(executorRole, bondingV5Address)).wait(); + console.log("✅ EXECUTOR_ROLE of FRouterV2 granted to BondingV5:", bondingV5Address); + + // Grant BONDING_ROLE of AgentFactoryV6 to BondingV5 + const bondingRole = await agentFactoryV6.BONDING_ROLE(); + await (await agentFactoryV6.grantRole(bondingRole, bondingV5Address)).wait(); + console.log("✅ BONDING_ROLE of AgentFactoryV6 granted to BondingV5:", bondingV5Address); + + // Set BondingV5 in AgentTax + await (await agentTax.setBondingV5(bondingV5Address)).wait(); + console.log("✅ AgentTax.setBondingV5() called:", bondingV5Address); + + console.log("\n✅ All role grants and configurations completed!"); // ============================================ // 5. Print Deployment Summary @@ -324,10 +298,11 @@ const { ethers, upgrades } = require("hardhat"); console.log("- LAUNCH_MODE_ACP_SKILL (2): Requires isAcpSkillLauncher authorization"); console.log("\n--- Deployment Order ---"); + console.log("0. ✅ deployLaunchpadv5_0.ts (AgentNftV2, AgentTax) - DONE"); console.log("1. ✅ deployLaunchpadv5_1.ts (FFactoryV2, FRouterV2) - DONE"); console.log("2. ✅ deployLaunchpadv5_2.ts (AgentFactoryV6) - DONE"); console.log("3. ✅ deployLaunchpadv5_3.ts (BondingConfig, BondingV5) - DONE"); - console.log("4. ⏳ deployLaunchpadv5_4.ts (Revoke deployer roles) - DONE"); + console.log("4. ⏳ deployLaunchpadv5_4.ts (Revoke deployer roles)"); console.log("\n" + "=".repeat(60)); diff --git a/scripts/launchpadv5/deployLaunchpadv5_4.ts b/scripts/launchpadv5/deployLaunchpadv5_4.ts index 12e6113..0538d1c 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_4.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_4.ts @@ -39,148 +39,111 @@ const { ethers } = require("hardhat"); if (!agentFactoryV6Address) { throw new Error("AGENT_FACTORY_V6_ADDRESS not set in environment"); } - - // AgentNftV2 is optional (only if newly deployed in deployPrerequisites_2.ts) const agentNftV2Address = process.env.AGENT_NFT_V2_ADDRESS; + if (!agentNftV2Address) { + throw new Error("AGENT_NFT_V2_ADDRESS not set in environment"); + } console.log("\nContract addresses:", { - fFactoryV2Address, - fRouterV2Address, - agentFactoryV6Address, - agentNftV2Address: agentNftV2Address || "(not set - skipping)", + fFactoryV2Address: fFactoryV2Address, + fRouterV2Address: fRouterV2Address, + agentFactoryV6Address: agentFactoryV6Address, + agentNftV2Address: agentNftV2Address, }); // ============================================ // Get contract instances // ============================================ - const fFactoryV2 = await ethers.getContractAt( - "FFactoryV2", - fFactoryV2Address - ); + const fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address); const fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address); - const agentFactoryV6 = await ethers.getContractAt( - "AgentFactoryV6", - agentFactoryV6Address - ); + const agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address); // ============================================ // Revoke FFactoryV2 deployer roles // ============================================ - console.log("\n--- Revoking FFactoryV2 deployer roles ---"); + console.log("\n--- Revoking deployer roles from FFactoryV2 ---"); const fFactoryAdminRole = await fFactoryV2.ADMIN_ROLE(); const fFactoryDefaultAdminRole = await fFactoryV2.DEFAULT_ADMIN_ROLE(); - // Check if deployer has ADMIN_ROLE if (await fFactoryV2.hasRole(fFactoryAdminRole, deployerAddress)) { - await ( - await fFactoryV2.revokeRole(fFactoryAdminRole, deployerAddress) - ).wait(); - console.log("✅ Revoked ADMIN_ROLE of FFactoryV2 from deployer"); + await (await fFactoryV2.revokeRole(fFactoryAdminRole, deployerAddress)).wait(); + console.log("✅ ADMIN_ROLE of FFactoryV2 revoked from deployer:", deployerAddress); } else { - console.log( - "⏭️ Deployer doesn't have ADMIN_ROLE on FFactoryV2 (already revoked)" - ); + console.log("⏭️ Deployer has no ADMIN_ROLE on FFactoryV2 (skip)"); } - // Check if deployer has DEFAULT_ADMIN_ROLE if (await fFactoryV2.hasRole(fFactoryDefaultAdminRole, deployerAddress)) { - await ( - await fFactoryV2.revokeRole(fFactoryDefaultAdminRole, deployerAddress) - ).wait(); - console.log("✅ Revoked DEFAULT_ADMIN_ROLE of FFactoryV2 from deployer"); + await (await fFactoryV2.revokeRole(fFactoryDefaultAdminRole, deployerAddress)).wait(); + console.log("✅ DEFAULT_ADMIN_ROLE of FFactoryV2 revoked from deployer:", deployerAddress); } else { - console.log( - "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on FFactoryV2 (already revoked)" - ); + console.log("⏭️ Deployer has no DEFAULT_ADMIN_ROLE on FFactoryV2 (skip)"); } // ============================================ // Revoke FRouterV2 deployer roles // ============================================ - console.log("\n--- Revoking FRouterV2 deployer roles ---"); + console.log("\n--- Revoking deployer roles from FRouterV2 ---"); const fRouterAdminRole = await fRouterV2.ADMIN_ROLE(); const fRouterDefaultAdminRole = await fRouterV2.DEFAULT_ADMIN_ROLE(); - // Check if deployer has ADMIN_ROLE if (await fRouterV2.hasRole(fRouterAdminRole, deployerAddress)) { - await ( - await fRouterV2.revokeRole(fRouterAdminRole, deployerAddress) - ).wait(); - console.log("✅ Revoked ADMIN_ROLE of FRouterV2 from deployer"); + await (await fRouterV2.revokeRole(fRouterAdminRole, deployerAddress)).wait(); + console.log("✅ ADMIN_ROLE of FRouterV2 revoked from deployer:", deployerAddress); } else { - console.log( - "⏭️ Deployer doesn't have ADMIN_ROLE on FRouterV2 (already revoked)" - ); + console.log("⏭️ Deployer has no ADMIN_ROLE on FRouterV2 (skip)"); } - // Check if deployer has DEFAULT_ADMIN_ROLE if (await fRouterV2.hasRole(fRouterDefaultAdminRole, deployerAddress)) { - await ( - await fRouterV2.revokeRole(fRouterDefaultAdminRole, deployerAddress) - ).wait(); - console.log("✅ Revoked DEFAULT_ADMIN_ROLE of FRouterV2 from deployer"); + await (await fRouterV2.revokeRole(fRouterDefaultAdminRole, deployerAddress)).wait(); + console.log("✅ DEFAULT_ADMIN_ROLE of FRouterV2 revoked from deployer:", deployerAddress); } else { - console.log( - "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on FRouterV2 (already revoked)" - ); + console.log("⏭️ Deployer has no DEFAULT_ADMIN_ROLE on FRouterV2 (skip)"); } // ============================================ // Revoke AgentFactoryV6 deployer roles // ============================================ - console.log("\n--- Revoking AgentFactoryV6 deployer roles ---"); - - const agentFactoryDefaultAdminRole = - await agentFactoryV6.DEFAULT_ADMIN_ROLE(); - - // Check if deployer has DEFAULT_ADMIN_ROLE - if ( - await agentFactoryV6.hasRole( - agentFactoryDefaultAdminRole, - deployerAddress - ) - ) { - await ( - await agentFactoryV6.revokeRole( - agentFactoryDefaultAdminRole, - deployerAddress - ) - ).wait(); - console.log( - "✅ Revoked DEFAULT_ADMIN_ROLE of AgentFactoryV6 from deployer" - ); + console.log("\n--- Revoking deployer roles from AgentFactoryV6 ---"); + + const agentFactoryDefaultAdminRole = await agentFactoryV6.DEFAULT_ADMIN_ROLE(); + + if (await agentFactoryV6.hasRole(agentFactoryDefaultAdminRole, deployerAddress)) { + await (await agentFactoryV6.revokeRole(agentFactoryDefaultAdminRole, deployerAddress)).wait(); + console.log("✅ DEFAULT_ADMIN_ROLE of AgentFactoryV6 revoked from deployer:", deployerAddress); } else { - console.log( - "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentFactoryV6 (already revoked)" - ); + console.log("⏭️ Deployer has no DEFAULT_ADMIN_ROLE on AgentFactoryV6 (skip)"); } // ============================================ - // Revoke AgentNftV2 deployer roles (if applicable) + // Revoke AgentNftV2 deployer roles // ============================================ - if (agentNftV2Address) { - console.log("\n--- Revoking AgentNftV2 deployer roles ---"); - const agentNftV2 = await ethers.getContractAt( - "AgentNftV2", - agentNftV2Address - ); - const agentNftDefaultAdminRole = await agentNftV2.DEFAULT_ADMIN_ROLE(); - - // Check if deployer has DEFAULT_ADMIN_ROLE - if (await agentNftV2.hasRole(agentNftDefaultAdminRole, deployerAddress)) { - await ( - await agentNftV2.revokeRole(agentNftDefaultAdminRole, deployerAddress) - ).wait(); - console.log( - "✅ Revoked DEFAULT_ADMIN_ROLE of AgentNftV2 from deployer" - ); - } else { - console.log( - "⏭️ Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentNftV2 (already revoked)" - ); - } + console.log("\n--- Revoking deployer roles from AgentNftV2 ---"); + const agentNftV2 = await ethers.getContractAt("AgentNftV2", agentNftV2Address); + + const agentNftDefaultAdminRole = await agentNftV2.DEFAULT_ADMIN_ROLE(); + if (await agentNftV2.hasRole(agentNftDefaultAdminRole, deployerAddress)) { + await (await agentNftV2.revokeRole(agentNftDefaultAdminRole, deployerAddress)).wait(); + console.log("✅ DEFAULT_ADMIN_ROLE of AgentNftV2 revoked from deployer:", deployerAddress); + } else { + console.log("⏭️ Deployer has no DEFAULT_ADMIN_ROLE on AgentNftV2 (skip)"); + } + + const agentNftAdminRole = await agentNftV2.ADMIN_ROLE(); + if (await agentNftV2.hasRole(agentNftAdminRole, deployerAddress)) { + await (await agentNftV2.revokeRole(agentNftAdminRole, deployerAddress)).wait(); + console.log("✅ ADMIN_ROLE of AgentNftV2 revoked from deployer:", deployerAddress); + } else { + console.log("⏭️ Deployer has no ADMIN_ROLE on AgentNftV2 (skip)"); + } + + const agentNftValidatorAdminRole = await agentNftV2.VALIDATOR_ADMIN_ROLE(); + if (await agentNftV2.hasRole(agentNftValidatorAdminRole, deployerAddress)) { + await (await agentNftV2.revokeRole(agentNftValidatorAdminRole, deployerAddress)).wait(); + console.log("✅ VALIDATOR_ADMIN_ROLE of AgentNftV2 revoked from deployer:", deployerAddress); + } else { + console.log("⏭️ Deployer has no VALIDATOR_ADMIN_ROLE on AgentNftV2 (skip)"); } // ============================================ @@ -199,11 +162,10 @@ const { ethers } = require("hardhat"); console.log("\n✅ Security hardening complete!"); console.log("\n--- Deployment Order ---"); + console.log("0. ✅ deployLaunchpadv5_0.ts (AgentNftV2, AgentTax) - DONE"); console.log("1. ✅ deployLaunchpadv5_1.ts (FFactoryV2, FRouterV2) - DONE"); console.log("2. ✅ deployLaunchpadv5_2.ts (AgentFactoryV6) - DONE"); - console.log( - "3. ✅ deployLaunchpadv5_3.ts (BondingConfig, BondingV5) - DONE" - ); + console.log("3. ✅ deployLaunchpadv5_3.ts (BondingConfig, BondingV5) - DONE"); console.log("4. ✅ deployLaunchpadv5_4.ts (Revoke deployer roles) - DONE"); console.log("\n" + "=".repeat(60)); @@ -217,7 +179,9 @@ const { ethers } = require("hardhat"); console.log(`- BondingV5: ${process.env.BONDING_V5_ADDRESS}`); console.log(`- Virtual Token: ${process.env.VIRTUAL_TOKEN_ADDRESS}`); console.log(`- AgentTokenV2: ${process.env.AGENT_TOKEN_V2_IMPLEMENTATION}`); - console.log(`- AgentVeTokenV2: ${process.env.AGENT_VE_TOKEN_V2_IMPLEMENTATION}`); + console.log( + `- AgentVeTokenV2: ${process.env.AGENT_VE_TOKEN_V2_IMPLEMENTATION}` + ); console.log(`- AgentDAOImpl: ${process.env.AGENT_DAO_IMPLEMENTATION}`); console.log(`- AgentNftV2: ${process.env.AGENT_NFT_V2_ADDRESS}`); console.log(`- AgentTaxManager: ${process.env.AGENT_TOKEN_TAX_MANAGER}`); diff --git a/scripts/launchpadv5/handle_agent_tax.ts b/scripts/launchpadv5/handle_agent_tax.ts index 9f1d603..32cf161 100644 --- a/scripts/launchpadv5/handle_agent_tax.ts +++ b/scripts/launchpadv5/handle_agent_tax.ts @@ -28,11 +28,10 @@ async function main() { // ============================================ /** - * AGENT_TAX_ADDRESS: The deployed AgentTax contract address - * Where to get: From your deployment output or .env file (e.g., AGENT_TAX_ADDRESS) + * agentTaxAddress: The deployed AgentTax contract address */ - const AGENT_TAX_ADDRESS = process.env.AGENT_TOKEN_TAX_MANAGER; - if (!AGENT_TAX_ADDRESS) { + const agentTaxAddress = process.env.AGENT_TOKEN_TAX_MANAGER; + if (!agentTaxAddress) { throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); } @@ -100,8 +99,8 @@ async function main() { console.log("Signer address:", signer.address); // Connect to AgentTax contract - const agentTax = new ethers.Contract(AGENT_TAX_ADDRESS, AGENT_TAX_ABI, signer); - console.log("AgentTax address:", AGENT_TAX_ADDRESS); + const agentTax = new ethers.Contract(agentTaxAddress, AGENT_TAX_ABI, signer); + console.log("Connected to AgentTax contract at:", agentTaxAddress); // Check if signer has EXECUTOR_ROLE const executorRole = await agentTax.EXECUTOR_ROLE(); From 8a539e03b727d14c35522d241c3023ec24388010 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Wed, 11 Mar 2026 14:21:19 +0800 Subject: [PATCH 4/8] add deployment_steps.md and rename AGENT_TOKEN_TAX_MANAGER to AGENT_TAX_CONTRACT_ADDRESS --- scripts/launchpadv2/deployLaunchpadv2.ts | 4 +- .../deployLaunchpadv2_ethSepolia.ts | 4 +- .../deployPrerequisites_ethSepolia.ts | 10 ++-- scripts/launchpadv5/AUDIT_SUMMARY.md | 4 +- scripts/launchpadv5/deployLaunchpadv5_0.ts | 14 +++--- scripts/launchpadv5/deployLaunchpadv5_2.ts | 47 +++++-------------- scripts/launchpadv5/deployLaunchpadv5_3.ts | 4 +- scripts/launchpadv5/deployLaunchpadv5_4.ts | 6 +-- scripts/launchpadv5/deployment_steps.md | 15 ++++++ scripts/launchpadv5/handle_agent_tax.ts | 4 +- 10 files changed, 50 insertions(+), 62 deletions(-) create mode 100644 scripts/launchpadv5/deployment_steps.md diff --git a/scripts/launchpadv2/deployLaunchpadv2.ts b/scripts/launchpadv2/deployLaunchpadv2.ts index 0a1f2fe..4804150 100644 --- a/scripts/launchpadv2/deployLaunchpadv2.ts +++ b/scripts/launchpadv2/deployLaunchpadv2.ts @@ -156,9 +156,9 @@ const { ethers, upgrades } = require("hardhat"); const agentTokenTaxManager = - process.env.AGENT_TOKEN_TAX_MANAGER; + process.env.AGENT_TAX_CONTRACT_ADDRESS; if (!agentTokenTaxManager) { - throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + throw new Error("AGENT_TAX_CONTRACT_ADDRESS not set in environment"); } console.log("Deployment arguments loaded:", { diff --git a/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts b/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts index b4b7fd4..cf632dc 100644 --- a/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts +++ b/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts @@ -160,9 +160,9 @@ const { ethers, upgrades } = require("hardhat"); const agentTokenTaxManager = - process.env.AGENT_TOKEN_TAX_MANAGER; + process.env.AGENT_TAX_CONTRACT_ADDRESS; if (!agentTokenTaxManager) { - throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + throw new Error("AGENT_TAX_CONTRACT_ADDRESS not set in environment"); } console.log("Deployment arguments loaded:", { diff --git a/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts b/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts index 0f93fa0..f594ca1 100644 --- a/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts +++ b/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts @@ -4,7 +4,7 @@ const { ethers, upgrades } = require("hardhat"); (async () => { try { console.log("\n=== Prerequisites Deployment Starting ==="); - console.log("This script deploys: AgentDAO, AgentNftV2, AgentTax (AGENT_TOKEN_TAX_MANAGER)"); + console.log("This script deploys: AgentDAO, AgentNftV2, AgentTax (AGENT_TAX_CONTRACT_ADDRESS)"); // Basic check for .env variables const deployerAddress = process.env.DEPLOYER; @@ -93,11 +93,11 @@ const { ethers, upgrades } = require("hardhat"); // MINTER_ROLE will be granted to AgentFactoryV6 in the main deployment script // ============================================ - // 3. Deploy AgentTax (AGENT_TOKEN_TAX_MANAGER) + // 3. Deploy AgentTax (AGENT_TAX_CONTRACT_ADDRESS) // ============================================ const agentNftV2Address = process.env.AGENT_NFT_V2; - console.log("\n--- Deploying AgentTax (AGENT_TOKEN_TAX_MANAGER) ---"); + console.log("\n--- Deploying AgentTax (AGENT_TAX_CONTRACT_ADDRESS) ---"); const AgentTax = await ethers.getContractFactory("AgentTax"); const agentTax = await upgrades.deployProxy( AgentTax, @@ -129,12 +129,12 @@ const { ethers, upgrades } = require("hardhat"); console.log("Copy the following addresses to your .env file:\n"); // console.log(`AGENT_DAO=${agentDAOAddress}`); console.log(`AGENT_NFT_V2=${agentNftV2Address}`); - console.log(`AGENT_TOKEN_TAX_MANAGER=${agentTaxAddress}`); + console.log(`AGENT_TAX_CONTRACT_ADDRESS=${agentTaxAddress}`); console.log("\n--- Full Summary ---"); // console.log("- AgentDAO (implementation):", agentDAOAddress); console.log("- AgentNftV2 (proxy):", agentNftV2Address); - console.log("- AgentTax (AGENT_TOKEN_TAX_MANAGER):", agentTaxAddress); + console.log("- AgentTax (AGENT_TAX_CONTRACT_ADDRESS):", agentTaxAddress); console.log("\n--- Manual Steps Required (by admin) ---"); console.log("AgentNftV2: MINTER_ROLE will be granted to AgentFactoryV6 in main deployment script"); diff --git a/scripts/launchpadv5/AUDIT_SUMMARY.md b/scripts/launchpadv5/AUDIT_SUMMARY.md index 70e5ce9..ba33422 100644 --- a/scripts/launchpadv5/AUDIT_SUMMARY.md +++ b/scripts/launchpadv5/AUDIT_SUMMARY.md @@ -373,7 +373,7 @@ Key test scenarios: **Usage:** ```bash -npx hardhat run scripts/launchpadv5/e2e_test.ts --network base_sepolia +npx hardhat run scripts/launchpadv5/e2e_test.ts --network ``` ### 8.2 deploy_agent_tax_swap.ts @@ -390,7 +390,7 @@ npx hardhat run scripts/launchpadv5/e2e_test.ts --network base_sepolia **Usage:** ```bash -npx hardhat run scripts/launchpadv5/deploy_agent_tax_swap.ts --network eth_sepolia +npx hardhat run scripts/launchpadv5/deploy_agent_tax_swap.ts --network ``` ### 8.3 handle_agent_tax.ts diff --git a/scripts/launchpadv5/deployLaunchpadv5_0.ts b/scripts/launchpadv5/deployLaunchpadv5_0.ts index 7338381..3111ad4 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_0.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_0.ts @@ -6,7 +6,7 @@ const { ethers, upgrades } = require("hardhat"); /** * Deploy prerequisites for FFactoryV2: * - AgentNftV2 (optional, if not already deployed) - * - AgentTax (AGENT_TOKEN_TAX_MANAGER / FFactoryV2_TAX_VAULT) + * - AgentTax (AGENT_TAX_CONTRACT_ADDRESS / FFactoryV2_TAX_VAULT) * * Run this script before deployLaunchpadv5_1.ts */ @@ -151,13 +151,13 @@ const { ethers, upgrades } = require("hardhat"); } // ============================================ - // 2. Deploy AgentTax (AGENT_TOKEN_TAX_MANAGER) + // 2. Deploy AgentTax (AGENT_TAX_CONTRACT_ADDRESS) // ============================================ - const existingAgentTax = process.env.AGENT_TOKEN_TAX_MANAGER; + const existingAgentTax = process.env.AGENT_TAX_CONTRACT_ADDRESS; let agentTaxAddress: string; if (!existingAgentTax) { - console.log("\n--- Deploying AgentTax (AGENT_TOKEN_TAX_MANAGER) ---"); + console.log("\n--- Deploying AgentTax (AGENT_TAX_CONTRACT_ADDRESS) ---"); const AgentTax = await ethers.getContractFactory("AgentTax"); const agentTax = await upgrades.deployProxy( AgentTax, @@ -179,7 +179,7 @@ const { ethers, upgrades } = require("hardhat"); await agentTax.waitForDeployment(); agentTaxAddress = await agentTax.getAddress(); console.log("AgentTax deployed at:", agentTaxAddress); - deployedContracts["AGENT_TOKEN_TAX_MANAGER"] = agentTaxAddress; + deployedContracts["AGENT_TAX_CONTRACT_ADDRESS"] = agentTaxAddress; // Grant EXECUTOR_V2_ROLE to BE_TAX_OPS_WALLETS (for updateCreator functions) const executorV2Role = await agentTax.EXECUTOR_V2_ROLE(); @@ -203,7 +203,7 @@ const { ethers, upgrades } = require("hardhat"); console.log("\n--- Using existing AgentTax ---"); console.log("AgentTax address:", existingAgentTax); agentTaxAddress = existingAgentTax; - reusedContracts["AGENT_TOKEN_TAX_MANAGER"] = existingAgentTax; + reusedContracts["AGENT_TAX_CONTRACT_ADDRESS"] = existingAgentTax; } // ============================================ @@ -230,7 +230,7 @@ const { ethers, upgrades } = require("hardhat"); console.log("\n--- Environment Variables for .env file ---"); console.log("# Prerequisites for FFactoryV2:"); console.log(`AGENT_NFT_V2_ADDRESS=${agentNftV2Address}`); - console.log(`AGENT_TOKEN_TAX_MANAGER=${agentTaxAddress}`); + console.log(`AGENT_TAX_CONTRACT_ADDRESS=${agentTaxAddress}`); console.log(`FFactoryV2_TAX_VAULT=${agentTaxAddress}`); console.log("\n--- Manual Steps Required (by admin) ---"); diff --git a/scripts/launchpadv5/deployLaunchpadv5_2.ts b/scripts/launchpadv5/deployLaunchpadv5_2.ts index 4dcf954..681696d 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_2.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_2.ts @@ -69,9 +69,9 @@ const { ethers, upgrades } = require("hardhat"); if (!sentientSellTax) { throw new Error("SENTIENT_SELL_TAX not set in environment"); } - const agentTokenTaxManager = process.env.AGENT_TOKEN_TAX_MANAGER; - if (!agentTokenTaxManager) { - throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + const agentTaxContractAddress = process.env.AGENT_TAX_CONTRACT_ADDRESS; + if (!agentTaxContractAddress) { + throw new Error("AGENT_TAX_CONTRACT_ADDRESS not set in environment"); } // REQUIRED: AgentNftV2 must be deployed first (in deployLaunchpadv5_0.ts) @@ -127,7 +127,7 @@ const { ethers, upgrades } = require("hardhat"); fRouterV2Address, sentientBuyTax, sentientSellTax, - agentTokenTaxManager, + agentTaxContractAddress, agentTokenV2Impl: agentTokenV2Impl || "(will deploy)", agentVeTokenV2Impl: agentVeTokenV2Impl || "(will deploy)", agentDAOImpl: agentDAOImpl || "(will deploy)", @@ -299,14 +299,14 @@ const { ethers, upgrades } = require("hardhat"); sentientBuyTax, sentientSellTax, taxSwapThresholdBasisPoints, - agentTokenTaxManager + agentTaxContractAddress ); await txSetTokenParams.wait(); console.log("AgentFactoryV6.setTokenParams() called:", { buyTax: sentientBuyTax, sellTax: sentientSellTax, taxSwapThreshold: taxSwapThresholdBasisPoints, - taxRecipient: agentTokenTaxManager, + taxRecipient: agentTaxContractAddress, }); // Grant DEFAULT_ADMIN_ROLE of AgentFactoryV6 to admin @@ -340,29 +340,11 @@ const { ethers, upgrades } = require("hardhat"); agentNftV2Address ); - // Check if deployer has admin role to grant MINTER_ROLE - const defaultAdminRole = await agentNftV2Contract.DEFAULT_ADMIN_ROLE(); - const hasAdminRole = await agentNftV2Contract.hasRole(defaultAdminRole, deployerAddress); - - if (hasAdminRole) { - // Grant MINTER_ROLE to AgentFactoryV6 - const minterRole = await agentNftV2Contract.MINTER_ROLE(); - const tx = await agentNftV2Contract.grantRole(minterRole, agentFactoryV6Address); - await tx.wait(); - console.log("MINTER_ROLE of AgentNftV2 granted to AgentFactoryV6:", agentFactoryV6Address); - } else { - console.log( - "\n⚠️ Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentNftV2" - ); - console.log(" Admin needs to grant MINTER_ROLE manually:"); - console.log( - ` AgentNftV2.grantRole(MINTER_ROLE, ${agentFactoryV6Address})` - ); - throw new Error("Deployer doesn't have DEFAULT_ADMIN_ROLE on AgentNftV2, need manually do"); - } - - // NOTE: Deployer roles are NOT revoked here - // Run deployRevokeRoles.ts after all deployments are complete + // Grant MINTER_ROLE to AgentFactoryV6 + const minterRole = await agentNftV2Contract.MINTER_ROLE(); + const tx = await agentNftV2Contract.grantRole(minterRole, agentFactoryV6Address); + await tx.wait(); + console.log("MINTER_ROLE of AgentNftV2 granted to AgentFactoryV6:", agentFactoryV6Address); // ============================================ // 10. Print Deployment Summary @@ -392,13 +374,6 @@ const { ethers, upgrades } = require("hardhat"); console.log(`- ${name}: ${address}`); } - console.log("\n--- Manual Steps Required (by admin) ---"); - if (!hasAdminRole) { - console.log( - `- Grant MINTER_ROLE on AgentNftV2 to AgentFactoryV6: ${agentFactoryV6Address}` - ); - } - console.log("\n--- Deployment Order ---"); console.log("0. ✅ deployLaunchpadv5_0.ts (AgentTax, AgentNftV2) - DONE"); console.log("1. ✅ deployLaunchpadv5_1.ts (FFactoryV2, FRouterV2) - DONE"); diff --git a/scripts/launchpadv5/deployLaunchpadv5_3.ts b/scripts/launchpadv5/deployLaunchpadv5_3.ts index 6623e61..0fc9cb8 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_3.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_3.ts @@ -86,9 +86,9 @@ const { ethers, upgrades } = require("hardhat"); if (!targetRealVirtual) { throw new Error("TARGET_REAL_VIRTUAL not set in environment"); } - const agentTaxAddress = process.env.AGENT_TOKEN_TAX_MANAGER; + const agentTaxAddress = process.env.AGENT_TAX_CONTRACT_ADDRESS; if (!agentTaxAddress) { - throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + throw new Error("AGENT_TAX_CONTRACT_ADDRESS not set in environment"); } const maxAirdropPercent = process.env.MAX_AIRDROP_PERCENT; diff --git a/scripts/launchpadv5/deployLaunchpadv5_4.ts b/scripts/launchpadv5/deployLaunchpadv5_4.ts index 0538d1c..8a5cdb9 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_4.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_4.ts @@ -179,12 +179,10 @@ const { ethers } = require("hardhat"); console.log(`- BondingV5: ${process.env.BONDING_V5_ADDRESS}`); console.log(`- Virtual Token: ${process.env.VIRTUAL_TOKEN_ADDRESS}`); console.log(`- AgentTokenV2: ${process.env.AGENT_TOKEN_V2_IMPLEMENTATION}`); - console.log( - `- AgentVeTokenV2: ${process.env.AGENT_VE_TOKEN_V2_IMPLEMENTATION}` - ); + console.log(`- AgentVeTokenV2: ${process.env.AGENT_VE_TOKEN_V2_IMPLEMENTATION}`); console.log(`- AgentDAOImpl: ${process.env.AGENT_DAO_IMPLEMENTATION}`); console.log(`- AgentNftV2: ${process.env.AGENT_NFT_V2_ADDRESS}`); - console.log(`- AgentTaxManager: ${process.env.AGENT_TOKEN_TAX_MANAGER}`); + console.log(`- AgentTaxContract: ${process.env.AGENT_TAX_CONTRACT_ADDRESS}`); } catch (e) { console.error("Role revocation failed:", e); throw e; diff --git a/scripts/launchpadv5/deployment_steps.md b/scripts/launchpadv5/deployment_steps.md new file mode 100644 index 0000000..7343dc8 --- /dev/null +++ b/scripts/launchpadv5/deployment_steps.md @@ -0,0 +1,15 @@ +### New EVM Chain (ETH Mainnet, BSC Mainnet, etc) ### +1. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_0.ts --network ` +2. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_1.ts --network ` +3. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_2.ts --network ` +4. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_3.ts --network ` +5. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_4.ts --network ` + + +### Existing Chain (only BASE Mainnet/Sepolia) ### +1. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_3.ts --network ` +2. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_4.ts --network `, but expect to have no actions cuz all contract's roles are alr revoked previously + + + + diff --git a/scripts/launchpadv5/handle_agent_tax.ts b/scripts/launchpadv5/handle_agent_tax.ts index 32cf161..3c88204 100644 --- a/scripts/launchpadv5/handle_agent_tax.ts +++ b/scripts/launchpadv5/handle_agent_tax.ts @@ -30,9 +30,9 @@ async function main() { /** * agentTaxAddress: The deployed AgentTax contract address */ - const agentTaxAddress = process.env.AGENT_TOKEN_TAX_MANAGER; + const agentTaxAddress = process.env.AGENT_TAX_CONTRACT_ADDRESS; if (!agentTaxAddress) { - throw new Error("AGENT_TOKEN_TAX_MANAGER not set in environment"); + throw new Error("AGENT_TAX_CONTRACT_ADDRESS not set in environment"); } /** From 65af9523b3d64fcec41ec1a6b51d00c290ddcbf3 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Thu, 12 Mar 2026 19:40:51 +0800 Subject: [PATCH 5/8] make airdropPercent related to bips --- .openzeppelin/base-sepolia.json | 776 +++++++++++++++- .openzeppelin/sepolia.json | 740 +++++++++++++++ contracts/launchpadv2/BondingConfig.sol | 69 +- contracts/launchpadv2/BondingV5.sol | 22 +- .../deployLaunchpadv2_ethSepolia.ts | 737 ++++----------- .../deployPrerequisites_ethSepolia.ts | 148 --- scripts/launchpadv2/e2e_test_bondingv2.ts | 847 ++++++++++++++++++ scripts/launchpadv5/deployLaunchpadv5_3.ts | 54 +- scripts/launchpadv5/e2e_test.ts | 21 +- test/launchpadv5/bondingV5.js | 152 ++-- 10 files changed, 2706 insertions(+), 860 deletions(-) delete mode 100644 scripts/launchpadv2/deployPrerequisites_ethSepolia.ts create mode 100644 scripts/launchpadv2/e2e_test_bondingv2.ts diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index 66bc57d..9af8340 100644 --- a/.openzeppelin/base-sepolia.json +++ b/.openzeppelin/base-sepolia.json @@ -770,6 +770,16 @@ "address": "0x08f745F2025532Cd602459d9973D8e978a10afD7", "txHash": "0x4af16094f36abe115cad9818402a78899dce3f1cef42d3626907f49afe1c1899", "kind": "transparent" + }, + { + "address": "0xfFBc8b7FDA99C8458b00b06C0c638D1Fa3Fb4a93", + "txHash": "0x2d9b121e6e10c281026b30861da9fcd8b51c9d860cb76b5fbe81b9a64408b5d4", + "kind": "transparent" + }, + { + "address": "0xb29cDBF221f211C0f7314E6F8aF6b5397Be9BE8d", + "txHash": "0x5a19c2fc7ec2ea0f47db5d180815c66f8bd2c30c9dce42e5ec31d9a4edb8f410", + "kind": "transparent" } ], "impls": { @@ -46761,9 +46771,725 @@ } } }, - "0e720901e0b9c0729eef6dbb079a4850508dfe038219365f4af1eb3d448b6953": { - "address": "0x42D494C79A3438ce9C433275eb9ff95D0c390074", - "txHash": "0x08994b26dabb9700ace1442aaef089ab1fd2ffe1da66b5276d4a7d8cd8ea132b", + "0e720901e0b9c0729eef6dbb079a4850508dfe038219365f4af1eb3d448b6953": { + "address": "0x42D494C79A3438ce9C433275eb9ff95D0c390074", + "txHash": "0x08994b26dabb9700ace1442aaef089ab1fd2ffe1da66b5276d4a7d8cd8ea132b", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(IFFactoryV2Minimal)1942", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:68" + }, + { + "label": "router", + "offset": 0, + "slot": "1", + "type": "t_contract(IFRouterV2Minimal)2004", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:69" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "2", + "type": "t_contract(IAgentFactoryV6Minimal)2069", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:70" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "3", + "type": "t_contract(BondingConfig)1909", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:71" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_struct(Token)1475_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:73" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "5", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:74" + }, + { + "label": "tokenLaunchParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_struct(LaunchParams)1486_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:80" + }, + { + "label": "tokenGradThreshold", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:83" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)158_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_contract(BondingConfig)1909": { + "label": "contract BondingConfig", + "numberOfBytes": "20" + }, + "t_contract(IAgentFactoryV6Minimal)2069": { + "label": "contract IAgentFactoryV6Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFFactoryV2Minimal)1942": { + "label": "contract IFFactoryV2Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFRouterV2Minimal)2004": { + "label": "contract IFRouterV2Minimal", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(LaunchParams)1486_storage)": { + "label": "mapping(address => struct BondingConfig.LaunchParams)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)1475_storage)": { + "label": "mapping(address => struct BondingConfig.Token)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)1436_storage": { + "label": "struct BondingConfig.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(LaunchParams)1486_storage": { + "label": "struct BondingConfig.LaunchParams", + "members": [ + { + "label": "launchMode", + "type": "t_uint8", + "offset": 0, + "slot": "0" + }, + { + "label": "airdropPercent", + "type": "t_uint8", + "offset": 1, + "slot": "0" + }, + { + "label": "needAcf", + "type": "t_bool", + "offset": 2, + "slot": "0" + }, + { + "label": "antiSniperTaxType", + "type": "t_uint8", + "offset": 3, + "slot": "0" + }, + { + "label": "isProject60days", + "type": "t_bool", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Token)1475_storage": { + "label": "struct BondingConfig.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)1436_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "f969325972d4f039759ad5e4c1368de28a605713dbe601fd82f36bafdd07c52d": { + "address": "0x8754Ee00a75572Ac0CC2C1879BdC11141bb69E8C", + "txHash": "0x14abe303aa0be07227018a4c226b3702835ca8f0f3313be29a9478d01e8ecb37", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "reserveSupplyParams", + "offset": 0, + "slot": "0", + "type": "t_struct(ReserveSupplyParams)1331_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:26" + }, + { + "label": "_scheduledLaunchParams", + "offset": 0, + "slot": "1", + "type": "t_struct(ScheduledLaunchParams)1350_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:42" + }, + { + "label": "teamTokenReservedWallet", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:45" + }, + { + "label": "isXLauncher", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:48" + }, + { + "label": "isAcpSkillLauncher", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:49" + }, + { + "label": "bondingCurveParams", + "offset": 0, + "slot": "7", + "type": "t_struct(BondingCurveParams)1368_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:56" + }, + { + "label": "_deployParams", + "offset": 0, + "slot": "9", + "type": "t_struct(DeployParams)1455_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:112" + }, + { + "label": "initialSupply", + "offset": 0, + "slot": "12", + "type": "t_uint256", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:115" + }, + { + "label": "feeTo", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:116" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_struct(BondingCurveParams)1368_storage": { + "label": "struct BondingConfig.BondingCurveParams", + "members": [ + { + "label": "fakeInitialVirtualLiq", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "targetRealVirtual", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(DeployParams)1455_storage": { + "label": "struct BondingConfig.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ReserveSupplyParams)1331_storage": { + "label": "struct BondingConfig.ReserveSupplyParams", + "members": [ + { + "label": "maxAirdropPercent", + "type": "t_uint16", + "offset": 0, + "slot": "0" + }, + { + "label": "maxTotalReservedPercent", + "type": "t_uint16", + "offset": 2, + "slot": "0" + }, + { + "label": "acfReservedPercent", + "type": "t_uint16", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ScheduledLaunchParams)1350_storage": { + "label": "struct BondingConfig.ScheduledLaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "normalLaunchFee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "acfFee", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "4c49e4da6c445a9085aea5b46964047bff754b045b78f35c21f79d0a018381f0": { + "address": "0x91Ba197bcAD4C92EEcA89F688Fb71c42A2E17692", + "txHash": "0x6156c3e9c18bbe642d8e8baac3c834c289b91f7a9cfa4aaa8c019973cce8c12f", "layout": { "solcVersion": "0.8.26", "storage": [ @@ -46771,7 +47497,7 @@ "label": "factory", "offset": 0, "slot": "0", - "type": "t_contract(IFFactoryV2Minimal)1942", + "type": "t_contract(IFFactoryV2Minimal)1990", "contract": "BondingV5", "src": "contracts/launchpadv2/BondingV5.sol:68" }, @@ -46779,7 +47505,7 @@ "label": "router", "offset": 0, "slot": "1", - "type": "t_contract(IFRouterV2Minimal)2004", + "type": "t_contract(IFRouterV2Minimal)2052", "contract": "BondingV5", "src": "contracts/launchpadv2/BondingV5.sol:69" }, @@ -46787,7 +47513,7 @@ "label": "agentFactory", "offset": 0, "slot": "2", - "type": "t_contract(IAgentFactoryV6Minimal)2069", + "type": "t_contract(IAgentFactoryV6Minimal)2117", "contract": "BondingV5", "src": "contracts/launchpadv2/BondingV5.sol:70" }, @@ -46795,7 +47521,7 @@ "label": "bondingConfig", "offset": 0, "slot": "3", - "type": "t_contract(BondingConfig)1909", + "type": "t_contract(BondingConfig)1957", "contract": "BondingV5", "src": "contracts/launchpadv2/BondingV5.sol:71" }, @@ -46803,7 +47529,7 @@ "label": "tokenInfo", "offset": 0, "slot": "4", - "type": "t_mapping(t_address,t_struct(Token)1475_storage)", + "type": "t_mapping(t_address,t_struct(Token)1435_storage)", "contract": "BondingV5", "src": "contracts/launchpadv2/BondingV5.sol:73" }, @@ -46819,7 +47545,7 @@ "label": "tokenLaunchParams", "offset": 0, "slot": "6", - "type": "t_mapping(t_address,t_struct(LaunchParams)1486_storage)", + "type": "t_mapping(t_address,t_struct(LaunchParams)1446_storage)", "contract": "BondingV5", "src": "contracts/launchpadv2/BondingV5.sol:80" }, @@ -46899,27 +47625,27 @@ "label": "uint8[]", "numberOfBytes": "32" }, - "t_contract(BondingConfig)1909": { + "t_contract(BondingConfig)1957": { "label": "contract BondingConfig", "numberOfBytes": "20" }, - "t_contract(IAgentFactoryV6Minimal)2069": { + "t_contract(IAgentFactoryV6Minimal)2117": { "label": "contract IAgentFactoryV6Minimal", "numberOfBytes": "20" }, - "t_contract(IFFactoryV2Minimal)1942": { + "t_contract(IFFactoryV2Minimal)1990": { "label": "contract IFFactoryV2Minimal", "numberOfBytes": "20" }, - "t_contract(IFRouterV2Minimal)2004": { + "t_contract(IFRouterV2Minimal)2052": { "label": "contract IFRouterV2Minimal", "numberOfBytes": "20" }, - "t_mapping(t_address,t_struct(LaunchParams)1486_storage)": { + "t_mapping(t_address,t_struct(LaunchParams)1446_storage)": { "label": "mapping(address => struct BondingConfig.LaunchParams)", "numberOfBytes": "32" }, - "t_mapping(t_address,t_struct(Token)1475_storage)": { + "t_mapping(t_address,t_struct(Token)1435_storage)": { "label": "mapping(address => struct BondingConfig.Token)", "numberOfBytes": "32" }, @@ -46931,7 +47657,7 @@ "label": "string", "numberOfBytes": "32" }, - "t_struct(Data)1436_storage": { + "t_struct(Data)1396_storage": { "label": "struct BondingConfig.Data", "members": [ { @@ -47009,7 +47735,7 @@ ], "numberOfBytes": "384" }, - "t_struct(LaunchParams)1486_storage": { + "t_struct(LaunchParams)1446_storage": { "label": "struct BondingConfig.LaunchParams", "members": [ { @@ -47020,32 +47746,32 @@ }, { "label": "airdropPercent", - "type": "t_uint8", + "type": "t_uint16", "offset": 1, "slot": "0" }, { "label": "needAcf", "type": "t_bool", - "offset": 2, + "offset": 3, "slot": "0" }, { "label": "antiSniperTaxType", "type": "t_uint8", - "offset": 3, + "offset": 4, "slot": "0" }, { "label": "isProject60days", "type": "t_bool", - "offset": 4, + "offset": 5, "slot": "0" } ], "numberOfBytes": "32" }, - "t_struct(Token)1475_storage": { + "t_struct(Token)1435_storage": { "label": "struct BondingConfig.Token", "members": [ { @@ -47074,7 +47800,7 @@ }, { "label": "data", - "type": "t_struct(Data)1436_storage", + "type": "t_struct(Data)1396_storage", "offset": 0, "slot": "4" }, @@ -47159,6 +47885,10 @@ ], "numberOfBytes": "896" }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, "t_uint8": { "label": "uint8", "numberOfBytes": "1" diff --git a/.openzeppelin/sepolia.json b/.openzeppelin/sepolia.json index d54cb21..9d3ebba 100644 --- a/.openzeppelin/sepolia.json +++ b/.openzeppelin/sepolia.json @@ -105,6 +105,26 @@ "address": "0x533F923dd5300e4fc821BE8FBD27a2baF060d7B9", "txHash": "0x3099bc673961bcd62f797cbe3d38958e0f084674055e3f5fff6c670d56b32aad", "kind": "transparent" + }, + { + "address": "0x2198D7E0E73551e78f3f7DC9a604168EdF6D375a", + "txHash": "0xcc1e9c310e4dac87c35648bbe6a8a731bacbe6e693d9504eb26c134db48b9849", + "kind": "transparent" + }, + { + "address": "0x3f050DDa9A5e32Ed93bE80a1FA71b4F7574EBF7F", + "txHash": "0xa1f23597ee8f585e88607d7de40ad95c62f72505d3f3055bc5e6f418f832d5f3", + "kind": "transparent" + }, + { + "address": "0xB61CFB9355B53F5ebF03E481111344E7aFB7Cc1F", + "txHash": "0x01cd82bb81cdcfa4e8e335fc83ab214c28423ceb4f1291c182cf2ae12b4b42b4", + "kind": "transparent" + }, + { + "address": "0x2cfe727226955595f3027709282850C04E03112B", + "txHash": "0x9a2fda7289756ac67bc6c5188f364c186162405aad7c7715f1322a81823bb7e1", + "kind": "transparent" } ], "impls": { @@ -3966,6 +3986,726 @@ ] } } + }, + "f969325972d4f039759ad5e4c1368de28a605713dbe601fd82f36bafdd07c52d": { + "address": "0x27a6c686758d483Fb13a478d103Fa63F7dfc5444", + "txHash": "0xd51a05380d7f378e15b1761a81544b2a05d6e641ac89815d1c699c925db905e8", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "reserveSupplyParams", + "offset": 0, + "slot": "0", + "type": "t_struct(ReserveSupplyParams)1331_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:26" + }, + { + "label": "_scheduledLaunchParams", + "offset": 0, + "slot": "1", + "type": "t_struct(ScheduledLaunchParams)1350_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:42" + }, + { + "label": "teamTokenReservedWallet", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:45" + }, + { + "label": "isXLauncher", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:48" + }, + { + "label": "isAcpSkillLauncher", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:49" + }, + { + "label": "bondingCurveParams", + "offset": 0, + "slot": "7", + "type": "t_struct(BondingCurveParams)1368_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:56" + }, + { + "label": "_deployParams", + "offset": 0, + "slot": "9", + "type": "t_struct(DeployParams)1455_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:112" + }, + { + "label": "initialSupply", + "offset": 0, + "slot": "12", + "type": "t_uint256", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:115" + }, + { + "label": "feeTo", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:116" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_struct(BondingCurveParams)1368_storage": { + "label": "struct BondingConfig.BondingCurveParams", + "members": [ + { + "label": "fakeInitialVirtualLiq", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "targetRealVirtual", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(DeployParams)1455_storage": { + "label": "struct BondingConfig.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ReserveSupplyParams)1331_storage": { + "label": "struct BondingConfig.ReserveSupplyParams", + "members": [ + { + "label": "maxAirdropPercent", + "type": "t_uint16", + "offset": 0, + "slot": "0" + }, + { + "label": "maxTotalReservedPercent", + "type": "t_uint16", + "offset": 2, + "slot": "0" + }, + { + "label": "acfReservedPercent", + "type": "t_uint16", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ScheduledLaunchParams)1350_storage": { + "label": "struct BondingConfig.ScheduledLaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "normalLaunchFee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "acfFee", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "4c49e4da6c445a9085aea5b46964047bff754b045b78f35c21f79d0a018381f0": { + "address": "0xE1Cb672D0c6D1544A0c031c2bda546174826E20E", + "txHash": "0xa5d351dd863a373e2a5c274aec71f5f6b47a925539aea9b6cec7cff1bebb7902", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(IFFactoryV2Minimal)1990", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:68" + }, + { + "label": "router", + "offset": 0, + "slot": "1", + "type": "t_contract(IFRouterV2Minimal)2052", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:69" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "2", + "type": "t_contract(IAgentFactoryV6Minimal)2117", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:70" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "3", + "type": "t_contract(BondingConfig)1957", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:71" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_struct(Token)1435_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:73" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "5", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:74" + }, + { + "label": "tokenLaunchParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_struct(LaunchParams)1446_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:80" + }, + { + "label": "tokenGradThreshold", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:83" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)158_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_contract(BondingConfig)1957": { + "label": "contract BondingConfig", + "numberOfBytes": "20" + }, + "t_contract(IAgentFactoryV6Minimal)2117": { + "label": "contract IAgentFactoryV6Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFFactoryV2Minimal)1990": { + "label": "contract IFFactoryV2Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFRouterV2Minimal)2052": { + "label": "contract IFRouterV2Minimal", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(LaunchParams)1446_storage)": { + "label": "mapping(address => struct BondingConfig.LaunchParams)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)1435_storage)": { + "label": "mapping(address => struct BondingConfig.Token)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)1396_storage": { + "label": "struct BondingConfig.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(LaunchParams)1446_storage": { + "label": "struct BondingConfig.LaunchParams", + "members": [ + { + "label": "launchMode", + "type": "t_uint8", + "offset": 0, + "slot": "0" + }, + { + "label": "airdropPercent", + "type": "t_uint16", + "offset": 1, + "slot": "0" + }, + { + "label": "needAcf", + "type": "t_bool", + "offset": 3, + "slot": "0" + }, + { + "label": "antiSniperTaxType", + "type": "t_uint8", + "offset": 4, + "slot": "0" + }, + { + "label": "isProject60days", + "type": "t_bool", + "offset": 5, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Token)1435_storage": { + "label": "struct BondingConfig.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)1396_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/contracts/launchpadv2/BondingConfig.sol b/contracts/launchpadv2/BondingConfig.sol index af7dd53..73d405c 100644 --- a/contracts/launchpadv2/BondingConfig.sol +++ b/contracts/launchpadv2/BondingConfig.sol @@ -8,7 +8,7 @@ import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; * @title BondingConfig * @notice Configuration contract for BondingV5 multi-chain launch modes * @dev Stores configurable parameters for different launch modes. - * gradThreshold is calculated per-token based on airdropPercent and needAcf. + * gradThreshold is calculated per-token based on airdropBips and needAcf. * project60days is handled by backend (not a separate launch mode). */ contract BondingConfig is Initializable, OwnableUpgradeable { @@ -17,9 +17,13 @@ contract BondingConfig is Initializable, OwnableUpgradeable { uint8 public constant LAUNCH_MODE_X_LAUNCH = 1; uint8 public constant LAUNCH_MODE_ACP_SKILL = 2; - // Reserve percentage constants - uint8 public constant MAX_TOTAL_RESERVED_PERCENT = 55; // At least 45% must remain in bonding curve - uint8 public constant ACF_RESERVED_PERCENT = 50; // ACF operations reserve 50% + // Reserve supply parameters struct (in bips, 1 bip = 0.01%, e.g., 5500 = 55.00%) + struct ReserveSupplyParams { + uint16 maxAirdropBips; // Maximum airdrop (e.g., 500 = 5.00%) + uint16 maxTotalReservedBips; // At least (100% - this) must remain in bonding curve (e.g., 5500 = 55.00%) + uint16 acfReservedBips; // ACF operations reserve (e.g., 5000 = 50.00%) + } + ReserveSupplyParams public reserveSupplyParams; // Anti-sniper tax type constants // These define the duration over which anti-sniper tax decreases from 99% to 0% @@ -40,9 +44,6 @@ contract BondingConfig is Initializable, OwnableUpgradeable { // Global wallet to receive reserved tokens (airdrop + ACF) address public teamTokenReservedWallet; - // Maximum airdrop percentage allowed (e.g., 5%) - uint8 public maxAirdropPercent; - // Authorized launchers for special modes mapping(address => bool) public isXLauncher; mapping(address => bool) public isAcpSkillLauncher; @@ -93,7 +94,7 @@ contract BondingConfig is Initializable, OwnableUpgradeable { // V5 configurable launch parameters (stored separately per token) struct LaunchParams { uint8 launchMode; - uint8 airdropPercent; + uint16 airdropBips; // in bips, 1 bip = 0.01% (e.g., 234 = 2.34%) bool needAcf; uint8 antiSniperTaxType; bool isProject60days; @@ -118,11 +119,11 @@ contract BondingConfig is Initializable, OwnableUpgradeable { event TeamTokenReservedWalletUpdated(address wallet); event CommonParamsUpdated(uint256 initialSupply, address feeTo); event BondingCurveParamsUpdated(BondingCurveParams params); - event MaxAirdropPercentUpdated(uint8 maxAirdropPercent); + event ReserveSupplyParamsUpdated(ReserveSupplyParams params); error InvalidAntiSniperType(); - error InvalidReservePercent(); - error AirdropPercentExceedsMax(); + error InvalidReserveBips(); + error AirdropBipsExceedsMax(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { @@ -133,7 +134,7 @@ contract BondingConfig is Initializable, OwnableUpgradeable { uint256 initialSupply_, address feeTo_, address teamTokenReservedWallet_, - uint8 maxAirdropPercent_, + ReserveSupplyParams memory reserveSupplyParams_, ScheduledLaunchParams memory scheduledLaunchParams_, DeployParams memory deployParams_, BondingCurveParams memory bondingCurveParams_ @@ -143,7 +144,7 @@ contract BondingConfig is Initializable, OwnableUpgradeable { initialSupply = initialSupply_; feeTo = feeTo_; teamTokenReservedWallet = teamTokenReservedWallet_; - maxAirdropPercent = maxAirdropPercent_; + reserveSupplyParams = reserveSupplyParams_; _scheduledLaunchParams = scheduledLaunchParams_; _deployParams = deployParams_; bondingCurveParams = bondingCurveParams_; @@ -186,25 +187,26 @@ contract BondingConfig is Initializable, OwnableUpgradeable { /** * @notice Calculate bonding curve supply with validation * @dev Validates: - * 1. airdropPercent_ <= maxAirdropPercent - * 2. airdropPercent + (needAcf ? 50 : 0) < MAX_TOTAL_RESERVED_PERCENT - * @param airdropPercent_ Airdrop percentage - * @param needAcf_ Whether ACF operations are needed (adds 50% reserve) + * 1. airdropBips_ <= reserveSupplyParams.maxAirdropBips + * 2. airdropBips + (needAcf ? acfReservedBips : 0) < maxTotalReservedBips + * All values are in bips (1 bip = 0.01%, e.g., 234 = 2.34%) + * @param airdropBips_ Airdrop in bips (e.g., 234 = 2.34%) + * @param needAcf_ Whether ACF operations are needed (adds acfReservedBips reserve) * @return bondingCurveSupply The supply available for bonding curve (in base units, not wei) */ function calculateBondingCurveSupply( - uint8 airdropPercent_, + uint16 airdropBips_, bool needAcf_ ) external view returns (uint256) { - if (airdropPercent_ > maxAirdropPercent) { - revert AirdropPercentExceedsMax(); + if (airdropBips_ > reserveSupplyParams.maxAirdropBips) { + revert AirdropBipsExceedsMax(); } - uint8 totalReserved = airdropPercent_ + - (needAcf_ ? ACF_RESERVED_PERCENT : 0); - if (totalReserved >= MAX_TOTAL_RESERVED_PERCENT) { - revert InvalidReservePercent(); + uint16 totalReserved = airdropBips_ + + (needAcf_ ? reserveSupplyParams.acfReservedBips : 0); + if (totalReserved >= reserveSupplyParams.maxTotalReservedBips) { + revert InvalidReserveBips(); } - return (initialSupply * (100 - totalReserved)) / 100; + return (initialSupply * (10000 - totalReserved)) / 10000; } /** @@ -283,12 +285,19 @@ contract BondingConfig is Initializable, OwnableUpgradeable { } /** - * @notice Set maximum airdrop percentage - * @param maxAirdropPercent_ The maximum airdrop percentage (e.g., 5 for 5%) + * @notice Set reserve supply parameters + * @param params_ The reserve supply parameters (all in bips, 1 bip = 0.01%, e.g., 500 = 5.00%) */ - function setMaxAirdropPercent(uint8 maxAirdropPercent_) external onlyOwner { - maxAirdropPercent = maxAirdropPercent_; - emit MaxAirdropPercentUpdated(maxAirdropPercent_); + function setReserveSupplyParams( + ReserveSupplyParams memory params_ + ) external onlyOwner { + require(params_.maxAirdropBips + params_.acfReservedBips <= params_.maxTotalReservedBips, InvalidReserveBips()); + require(params_.maxAirdropBips <= 10000, InvalidReserveBips()); + require(params_.maxTotalReservedBips <= 10000, InvalidReserveBips()); + require(params_.acfReservedBips <= 10000, InvalidReserveBips()); + + reserveSupplyParams = params_; + emit ReserveSupplyParamsUpdated(params_); } /** diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 99dfa2b..317a266 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -79,7 +79,7 @@ contract BondingV5 is // Mapping to store configurable launch parameters for each token mapping(address => BondingConfig.LaunchParams) public tokenLaunchParams; - // Mapping to store graduation threshold for each token (calculated per-token based on airdropPercent and needAcf) + // Mapping to store graduation threshold for each token (calculated per-token based on airdropBips and needAcf) mapping(address => uint256) public tokenGradThreshold; event PreLaunched( @@ -143,14 +143,14 @@ contract BondingV5 is uint256 purchaseAmount_, uint256 startTime_, uint8 launchMode_, - uint8 airdropPercent_, + uint16 airdropBips_, bool needAcf_, uint8 antiSniperTaxType_, bool isProject60days_ ) public nonReentrant returns (address, address, uint, uint256) { - // Fail-fast: validate reserve percentages and calculate bonding curve supply upfront - // This validates: airdropPercent <= maxAirdropPercent AND totalReserved < MAX_TOTAL_RESERVED_PERCENT - uint256 bondingCurveSupplyBase = bondingConfig.calculateBondingCurveSupply(airdropPercent_, needAcf_); + // Fail-fast: validate reserve bips and calculate bonding curve supply upfront + // This validates: airdropBips <= maxAirdropBips AND totalReserved < maxTotalReservedBips + uint256 bondingCurveSupplyBase = bondingConfig.calculateBondingCurveSupply(airdropBips_, needAcf_); // Validate anti-sniper tax type if (!bondingConfig.isValidAntiSniperType(antiSniperTaxType_)) { @@ -172,7 +172,7 @@ contract BondingV5 is _validateLaunchMode( launchMode_, antiSniperTaxType_, - airdropPercent_, + airdropBips_, needAcf_, isProject60days_, isScheduledLaunch @@ -303,7 +303,7 @@ contract BondingV5 is // Store V5 configurable launch parameters tokenLaunchParams[token] = BondingConfig.LaunchParams({ launchMode: launchMode_, - airdropPercent: airdropPercent_, + airdropBips: airdropBips_, needAcf: needAcf_, antiSniperTaxType: antiSniperTaxType_, isProject60days: isProject60days_ @@ -522,7 +522,7 @@ contract BondingV5 is tokenInfo[tokenAddress_].data.lastUpdated = block.timestamp; } - // Get per-token gradThreshold (calculated during preLaunch based on airdropPercent and needAcf) + // Get per-token gradThreshold (calculated during preLaunch based on airdropBips and needAcf) uint256 gradThreshold = tokenGradThreshold[tokenAddress_]; if ( @@ -669,7 +669,7 @@ contract BondingV5 is function _validateLaunchMode( uint8 launchMode_, uint8 antiSniperTaxType_, - uint8 airdropPercent_, + uint16 airdropBips_, bool needAcf_, bool isProject60days_, bool isScheduledLaunch_ @@ -698,12 +698,12 @@ contract BondingV5 is // Special modes require: // 1. ANTI_SNIPER_NONE // 2. Immediate launch (startTime within 24h) - // 3. airdropPercent = 0 + // 3. airdropBips = 0 // 4. needAcf = false // 5. isProject60days_ = false if (antiSniperTaxType_ != bondingConfig.ANTI_SNIPER_NONE() || isScheduledLaunch_ || - airdropPercent_ != 0 || + airdropBips_ != 0 || needAcf_ || isProject60days_) { revert InvalidSpecialLaunchParams(); diff --git a/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts b/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts index cf632dc..900ae1d 100644 --- a/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts +++ b/scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts @@ -1,366 +1,140 @@ -import { parseEther } from "ethers"; +/** + * Deploy BondingV2 and grant necessary roles + * + * This script reuses contracts already deployed by launchpadv5 scripts: + * - FFactoryV2, FRouterV2, AgentFactoryV6 (from deployLaunchpadv5_1.ts, deployLaunchpadv5_2.ts) + * + * Usage: + * npx hardhat run scripts/launchpadv2/deployLaunchpadv2_ethSepolia.ts --network eth_sepolia + * + * Required env vars: + * - FFactoryV2_ADDRESS, FRouterV2_ADDRESS, AGENT_FACTORY_V6_ADDRESS (existing contracts) + * - ADMIN_PRIVATE_KEY (for granting roles on existing contracts) + * - CONTRACT_CONTROLLER (for BondingV2 ownership) + * - BondingV2 init params: LAUNCHPAD_V2_CREATION_FEE_TO_ADDRESS, LAUNCHPAD_V2_FEE_AMOUNT, + * INITIAL_SUPPLY, ASSET_RATE, MAX_TX, GRAD_THRESHOLD, LAUNCHPAD_V2_START_TIME_DELAY + * - BondingV2 deploy params: TBA_SALT, TBA_IMPLEMENTATION, DAO_VOTING_PERIOD, DAO_THRESHOLD + * - BondingV2 launch params: TEAM_TOKEN_RESERVED_SUPPLY, TEAM_TOKEN_RESERVED_WALLET + */ + const { ethers, upgrades } = require("hardhat"); (async () => { try { - console.log("\n=== NewLaunchpad Deployment Starting ==="); + console.log( + "\n=== BondingV2 Deployment (Reusing LaunchpadV5 Contracts) ===" + ); + + // ============================================ + // 1. Load existing contract addresses + // ============================================ + const fFactoryV2Address = process.env.FFactoryV2_ADDRESS; + if (!fFactoryV2Address) throw new Error("FFactoryV2_ADDRESS not set"); + + const fRouterV2Address = process.env.FRouterV2_ADDRESS; + if (!fRouterV2Address) throw new Error("FRouterV2_ADDRESS not set"); + + const agentFactoryV6Address = process.env.AGENT_FACTORY_V6_ADDRESS; + if (!agentFactoryV6Address) + throw new Error("AGENT_FACTORY_V6_ADDRESS not set"); - // Basic check for .env variables - const deployerAddress = process.env.DEPLOYER; - if (!deployerAddress) { - throw new Error("DEPLOYER not set in environment"); - } - const beOpsWallet = process.env.BE_OPS_WALLET; - if (!beOpsWallet) { - throw new Error("BE_OPS_WALLET not set in environment"); - } const contractController = process.env.CONTRACT_CONTROLLER; - if (!contractController) { - throw new Error("CONTRACT_CONTROLLER not set in environment"); - } - const admin = process.env.ADMIN; - if (!admin) { - throw new Error("ADMIN not set in environment"); - } + if (!contractController) throw new Error("CONTRACT_CONTROLLER not set"); - // Load arguments directly from environment variables - const virtualToken = process.env.BRIDGED_TOKEN; - if (!virtualToken) { - throw new Error("BRIDGED_TOKEN not set in environment"); - } - const prototypeBuyTax = process.env.PROTOTYPE_BUY_TAX; - if (!prototypeBuyTax) { - throw new Error("PROTOTYPE_BUY_TAX not set in environment"); - } - const prototypeSellTax = process.env.PROTOTYPE_SELL_TAX; - if (!prototypeSellTax) { - throw new Error("PROTOTYPE_SELL_TAX not set in environment"); - } + const adminPrivateKey = process.env.ADMIN_PRIVATE_KEY; + if (!adminPrivateKey) + throw new Error( + "ADMIN_PRIVATE_KEY not set (needed for granting roles on existing contracts)" + ); - const sentientBuyTax = process.env.SENTIENT_BUY_TAX; - if (!sentientBuyTax) { - throw new Error("SENTIENT_BUY_TAX not set in environment"); - } - const sentientSellTax = process.env.SENTIENT_SELL_TAX; - if (!sentientSellTax) { - throw new Error("SENTIENT_SELL_TAX not set in environment"); - } - const antiSniperBuyTaxStartValue = - process.env.ANTI_SNIPER_BUY_TAX_START_VALUE; - if (!antiSniperBuyTaxStartValue) { - throw new Error("ANTI_SNIPER_BUY_TAX_START_VALUE not set in environment"); - } + // BondingV2 initialize params const creationFeeToAddress = process.env.LAUNCHPAD_V2_CREATION_FEE_TO_ADDRESS; - if (!creationFeeToAddress) { - throw new Error("LAUNCHPAD_V2_FEE_ADDRESS not set in environment"); - } + if (!creationFeeToAddress) + throw new Error("LAUNCHPAD_V2_CREATION_FEE_TO_ADDRESS not set"); + const feeAmount = process.env.LAUNCHPAD_V2_FEE_AMOUNT; - if (!feeAmount) { - throw new Error("LAUNCHPAD_V2_FEE_AMOUNT not set in environment"); - } + if (!feeAmount) throw new Error("LAUNCHPAD_V2_FEE_AMOUNT not set"); const initialSupply = process.env.INITIAL_SUPPLY; - if (!initialSupply) { - throw new Error("INITIAL_SUPPLY not set in environment"); - } + if (!initialSupply) throw new Error("INITIAL_SUPPLY not set"); const assetRate = process.env.ASSET_RATE; - if (!assetRate) { - throw new Error("ASSET_RATE not set in environment"); - } + if (!assetRate) throw new Error("ASSET_RATE not set"); const maxTx = process.env.MAX_TX; - if (!maxTx) { - throw new Error("MAX_TX not set in environment"); - } + if (!maxTx) throw new Error("MAX_TX not set"); const gradThreshold = process.env.GRAD_THRESHOLD; - if (!gradThreshold) { - throw new Error("GRAD_THRESHOLD not set in environment"); - } + if (!gradThreshold) throw new Error("GRAD_THRESHOLD not set"); + const startTimeDelay = process.env.LAUNCHPAD_V2_START_TIME_DELAY; - if (!startTimeDelay) { - throw new Error("LAUNCHPAD_V2_START_TIME_DELAY not set in environment"); - } + if (!startTimeDelay) + throw new Error("LAUNCHPAD_V2_START_TIME_DELAY not set"); + + // BondingV2 deploy params const tbaSalt = process.env.TBA_SALT; - if (!tbaSalt) { - throw new Error("TBA_SALT not set in environment"); - } - const tbaRegistry = process.env.TBA_REGISTRY; - if (!tbaRegistry) { - throw new Error("TBA_REGISTRY not set in environment"); - } + if (!tbaSalt) throw new Error("TBA_SALT not set"); + const tbaImplementation = process.env.TBA_IMPLEMENTATION; - if (!tbaImplementation) { - throw new Error("TBA_IMPLEMENTATION not set in environment"); - } + if (!tbaImplementation) throw new Error("TBA_IMPLEMENTATION not set"); + const daoVotingPeriod = process.env.DAO_VOTING_PERIOD; - if (!daoVotingPeriod) { - throw new Error("DAO_VOTING_PERIOD not set in environment"); - } + if (!daoVotingPeriod) throw new Error("DAO_VOTING_PERIOD not set"); const daoThreshold = process.env.DAO_THRESHOLD; - if (!daoThreshold) { - throw new Error("DAO_THRESHOLD not set in environment"); - } + if (!daoThreshold) throw new Error("DAO_THRESHOLD not set"); + + // BondingV2 launch params const teamTokenReservedSupply = process.env.TEAM_TOKEN_RESERVED_SUPPLY; - if (!teamTokenReservedSupply) { - throw new Error("TEAM_TOKEN_RESERVED_SUPPLY not set in environment"); - } + if (!teamTokenReservedSupply) + throw new Error("TEAM_TOKEN_RESERVED_SUPPLY not set"); const teamTokenReservedWallet = process.env.TEAM_TOKEN_RESERVED_WALLET; - if (!teamTokenReservedWallet) { - throw new Error("TEAM_TOKEN_RESERVED_WALLET not set in environment"); - } - const uniswapV2Factory = process.env.UNISWAP_V2_FACTORY; - if (!uniswapV2Factory) { - throw new Error("UNISWAP_V2_FACTORY not set in environment"); - } - const uniswapV2Router = process.env.UNISWAP_V2_ROUTER; - if (!uniswapV2Router) { - throw new Error("UNISWAP_V2_ROUTER not set in environment"); - } - const agentNftV2 = process.env.AGENT_NFT_V2; - if (!agentNftV2) { - throw new Error("AGENT_NFT_V2 not set in environment"); - } - // FFactoryV2_TAX_VAULT must be a taxManager contract, but if we can - // make sure FFactoryV2_TAX_VAULT != FRouter.taxManager, then FFactoryV2_TAX_VAULT can be an EOA wallet - const fFactoryV2TaxVault = process.env.FFactoryV2_TAX_VAULT; - if (!fFactoryV2TaxVault) { - throw new Error("FFactoryV2_TAX_VAULT not set in environment"); - } - const fRouterV2TaxManager = process.env.FRouterV2_TAX_MANAGER; - if (!fRouterV2TaxManager) { - throw new Error("FRouterV2_TAX_MANAGER not set in environment"); - } - const agentDAO = process.env.AGENT_DAO; - if (!agentDAO) { - throw new Error("AGENT_DAO not set in environment"); - } - const agentFactoryV6Vault = process.env.AGENT_FACTORY_V6_VAULT; - if (!agentFactoryV6Vault) { - throw new Error("AGENT_FACTORY_V6_VAULT not set in environment"); - } - const agentFactoryV6MaturityDuration = - process.env.AGENT_FACTORY_V6_Maturity_Duration; - if (!agentFactoryV6MaturityDuration) { - throw new Error( - "AGENT_FACTORY_V6_Maturity_Duration not set in environment" - ); - } - const agentFactoryV6NextId = process.env.AGENT_FACTORY_V6_NEXT_ID; - if (!agentFactoryV6NextId) { - throw new Error("AGENT_FACTORY_V6_NEXT_ID not set in environment"); - } - const antiSniperTaxVaultAddress = process.env.ANTI_SNIPER_TAX_VAULT; - if (!antiSniperTaxVaultAddress) { - throw new Error("ANTI_SNIPER_TAX_VAULT not set in environment"); - } - const taxSwapThresholdBasisPoints = - process.env.TAX_SWAP_THRESHOLD_BASIS_POINTS; - if (!taxSwapThresholdBasisPoints) { - throw new Error("TAX_SWAP_THRESHOLD_BASIS_POINTS not set in environment"); - } - - - const agentTokenTaxManager = - process.env.AGENT_TAX_CONTRACT_ADDRESS; - if (!agentTokenTaxManager) { - throw new Error("AGENT_TAX_CONTRACT_ADDRESS not set in environment"); - } - - console.log("Deployment arguments loaded:", { - virtualToken, - buyTax: prototypeBuyTax, - sellTax: prototypeSellTax, - factoryBuyTax: sentientBuyTax, - factorySellTax: sentientSellTax, - antiSniperBuyTaxStartValue, - feeAddress: creationFeeToAddress, - feeAmount, - initialSupply, - assetRate, - maxTx, - gradThreshold, - startTimeDelay, - tbaSalt, - tbaRegistry, - tbaImplementation, - daoVotingPeriod, - daoThreshold, - teamTokenReservedSupply, - uniswapV2Factory, - uniswapV2Router, - agentNftV2, - fFactoryV2TaxVault: fFactoryV2TaxVault, - fRouterV2TaxManager: fRouterV2TaxManager, - agentDAO, - agentFactoryV6Vault, - agentFactoryV6MaturityDuration, - agentFactoryV6NextId, - antiSniperTaxVaultAddress, - taxSwapThresholdBasisPoints, - agentTokenTaxManager - }); - - // 1. Deploy FFactoryV2, must happen before FRouterV2, - // because FRouterV2 cannot setFactory later - console.log("\n--- Deploying FFactoryV2 ---"); + if (!teamTokenReservedWallet) + throw new Error("TEAM_TOKEN_RESERVED_WALLET not set"); + + console.log("\n--- Configuration ---"); + console.log("Reusing existing contracts:"); + console.log(" FFactoryV2:", fFactoryV2Address); + console.log(" FRouterV2:", fRouterV2Address); + console.log(" AgentFactoryV6:", agentFactoryV6Address); + console.log("\nBondingV2 params:"); + console.log(" creationFeeToAddress:", creationFeeToAddress); + console.log(" feeAmount:", feeAmount); + console.log(" initialSupply:", initialSupply); + console.log(" assetRate:", assetRate); + console.log(" maxTx:", maxTx); + console.log(" gradThreshold:", gradThreshold); + console.log(" startTimeDelay:", startTimeDelay); + console.log(" tbaSalt:", tbaSalt); + console.log(" tbaImplementation:", tbaImplementation); + console.log(" daoVotingPeriod:", daoVotingPeriod); + console.log(" daoThreshold:", daoThreshold); + console.log(" teamTokenReservedSupply:", teamTokenReservedSupply); + console.log(" teamTokenReservedWallet:", teamTokenReservedWallet); + + // ============================================ + // 2. Setup signers + // ============================================ + const [deployer] = await ethers.getSigners(); + console.log("\nDeployer:", await deployer.getAddress()); + + const adminSigner = new ethers.Wallet(adminPrivateKey, ethers.provider); + console.log("Admin signer:", await adminSigner.getAddress()); + + // ============================================ + // 3. Get existing contract instances (with admin signer for role grants) + // ============================================ const FFactoryV2 = await ethers.getContractFactory("FFactoryV2"); - const fFactoryV2 = await upgrades.deployProxy( - FFactoryV2, - [ - fFactoryV2TaxVault, // taxVault - prototypeBuyTax, // buyTax - prototypeSellTax, // sellTax - antiSniperBuyTaxStartValue, // antiSniperBuyTaxStartValue - antiSniperTaxVaultAddress, // antiSniperTaxVault - ], - { - initializer: "initialize", - initialOwner: process.env.CONTRACT_CONTROLLER, - } - ); - await fFactoryV2.waitForDeployment(); - const fFactoryV2Address = await fFactoryV2.getAddress(); - console.log("FFactoryV2 deployed at:", fFactoryV2Address); + const fFactoryV2 = + FFactoryV2.attach(fFactoryV2Address).connect(adminSigner); - // 2. Deploy FRouterV2 - console.log("\n--- Deploying FRouterV2 ---"); const FRouterV2 = await ethers.getContractFactory("FRouterV2"); - const fRouterV2 = await upgrades.deployProxy( - FRouterV2, - [ - fFactoryV2Address, // factory - virtualToken, // virtualToken (assetToken) - ], - { - initializer: "initialize", - initialOwner: process.env.CONTRACT_CONTROLLER, - } - ); - await fRouterV2.waitForDeployment(); - const fRouterV2Address = await fRouterV2.getAddress(); - console.log("FRouterV2 deployed at:", fRouterV2Address); - - // 3. Grant ADMIN_ROLE of FFactoryV2 to deployer temporarily, for setRouter and setTaxParams - console.log( - "\n--- Granting ADMIN_ROLE of FFactoryV2 to deployer temporarily ---" - ); - const signers = await ethers.getSigners(); - const deployer = signers[0]; - const tx0 = await fFactoryV2.grantRole( - await fFactoryV2.ADMIN_ROLE(), - await deployer.getAddress() - ); - await tx0.wait(); - console.log("ADMIN_ROLE of FFactoryV2 granted to deployer temporarily"); - - // 4.0 Grant ADMIN_ROLE of FFactoryV2 to ADMIN, for setRouter and setTaxParams - console.log("\n--- Granting ADMIN_ROLE to ADMIN ---"); - const tx0_5 = await fFactoryV2.grantRole( - await fFactoryV2.ADMIN_ROLE(), - process.env.ADMIN - ); - await tx0_5.wait(); - console.log("ADMIN_ROLE granted to ADMIN"); - - // 4.1 set fRouterV2Address in FFactoryV2 - console.log("\n--- Setting fRouterV2Address in FFactoryV2 ---"); - const tx1 = await fFactoryV2.setRouter(fRouterV2Address); - await tx1.wait(); - console.log("fRouterV2Address set in FFactoryV2"); - - // 7. Grant ADMIN_ROLE of fRouterV2 to deployer temporarily, for setTaxManager and setAntiSniperTaxManager - console.log( - "\n--- Granting ADMIN_ROLE of fRouterV2 to deployer temporarily ---" - ); - const tx3_1 = await fRouterV2.grantRole( - await fRouterV2.ADMIN_ROLE(), - await deployer.getAddress() - ); - await tx3_1.wait(); - console.log("ADMIN_ROLE of fRouterV2 granted to deployer temporarily"); - - // 8. Set taxManager for FRouterV2 - console.log("\n--- Setting TaxManager for FRouterV2 ---"); - const tx4_1 = await fRouterV2.setTaxManager(fRouterV2TaxManager); - await tx4_1.wait(); - console.log("TaxManager set for FRouterV2: ", fRouterV2TaxManager); - - // 9. Set antiSniperTaxManager for FRouterV2 - console.log("\n--- Setting AntiSniperTaxManager for FRouterV2 ---"); - const tx5_1 = await fRouterV2.setAntiSniperTaxManager( - antiSniperTaxVaultAddress - ); - await tx5_1.wait(); - console.log("AntiSniperTaxManager set for FRouterV2"); + const fRouterV2 = FRouterV2.attach(fRouterV2Address).connect(adminSigner); - // 10. Deploy AgentTokenV2 implementation - console.log("\n--- Deploying AgentTokenV2 implementation ---"); - const AgentTokenV2 = await ethers.getContractFactory("AgentTokenV2"); - const agentTokenV2 = await AgentTokenV2.deploy(); - await agentTokenV2.waitForDeployment(); - const agentTokenV2Address = await agentTokenV2.getAddress(); - console.log( - "AgentTokenV2 implementation deployed at:", - agentTokenV2Address - ); - - // 11. Deploy AgentVeTokenV2 implementation - console.log("\n--- Deploying AgentVeTokenV2 implementation ---"); - const AgentVeTokenV2 = await ethers.getContractFactory("AgentVeTokenV2"); - const agentVeTokenV2 = await AgentVeTokenV2.deploy(); - await agentVeTokenV2.waitForDeployment(); - const agentVeTokenV2Address = await agentVeTokenV2.getAddress(); - console.log( - "AgentVeTokenV2 implementation deployed at:", - agentVeTokenV2Address - ); - - // 12. Deploy AgentFactoryV6 - console.log("\n--- Deploying AgentFactoryV6 ---"); const AgentFactoryV6 = await ethers.getContractFactory("AgentFactoryV6"); - const agentFactoryV6 = await upgrades.deployProxy( - AgentFactoryV6, - [ - agentTokenV2Address, // tokenImplementation_ - agentVeTokenV2Address, // veTokenImplementation_ - agentDAO, // daoImplementation_ - tbaRegistry, // tbaRegistry_ - virtualToken, // assetToken_ (virtualToken) - agentNftV2, // nft_ (AgentNftV2) - agentFactoryV6Vault, // vault_, who will hold all the NFTs - agentFactoryV6NextId, // nextId_ - ], - { - initializer: "initialize", - initialOwner: process.env.CONTRACT_CONTROLLER, - } - ); - await agentFactoryV6.waitForDeployment(); - const agentFactoryV6Address = await agentFactoryV6.getAddress(); - console.log("AgentFactoryV6 deployed at:", agentFactoryV6Address); - - // 14. Grant DEFAULT_ADMIN_ROLE to deployer temporarily for AgentFactoryV6, for setParams() - console.log( - "\n--- Granting DEFAULT_ADMIN_ROLE to deployer temporarily for AgentFactoryV6 ---" - ); - const tx7_1 = await agentFactoryV6.grantRole( - await agentFactoryV6.DEFAULT_ADMIN_ROLE(), - await deployer.getAddress() + const agentFactoryV6 = AgentFactoryV6.attach(agentFactoryV6Address).connect( + adminSigner ); - await tx7_1.wait(); - console.log("DEFAULT_ADMIN_ROLE granted to deployer"); - // 15. setParams() for AgentFactoryV6 - console.log("\n--- Setting params for AgentFactoryV6 ---"); - const tx8_1 = await agentFactoryV6.setParams( - agentFactoryV6MaturityDuration, // maturityDuration - uniswapV2Router, // uniswapRouter - process.env.ADMIN, // defaultDelegatee, - process.env.ADMIN // tokenAdmin, - ); - await tx8_1.wait(); - console.log("setParams() called successfully for AgentFactoryV6"); - - // 18. Deploy BondingV2 + // ============================================ + // 4. Deploy BondingV2 + // ============================================ console.log("\n--- Deploying BondingV2 ---"); const BondingV2 = await ethers.getContractFactory("BondingV2"); const bondingV2 = await upgrades.deployProxy( @@ -368,8 +142,8 @@ const { ethers, upgrades } = require("hardhat"); [ fFactoryV2Address, // factory_ fRouterV2Address, // router_ - creationFeeToAddress, // feeTo_ (fee address) - feeAmount, // fee_ (fee amount) + creationFeeToAddress, // feeTo_ + feeAmount, // fee_ initialSupply, // initialSupply_ assetRate, // assetRate_ maxTx, // maxTx_ @@ -379,252 +153,103 @@ const { ethers, upgrades } = require("hardhat"); ], { initializer: "initialize", - initialOwner: process.env.CONTRACT_CONTROLLER, + initialOwner: contractController, } ); await bondingV2.waitForDeployment(); const bondingV2Address = await bondingV2.getAddress(); console.log("BondingV2 deployed at:", bondingV2Address); - // 17. Set token params for AgentFactoryV6 - console.log("\n--- Setting token params for AgentFactoryV6 ---"); - const tx3 = await agentFactoryV6.setTokenParams( - sentientBuyTax, // projectBuyTaxBasisPoints (sentientBuyTax) - sentientSellTax, // projectSellTaxBasisPoints (sentientSellTax) - taxSwapThresholdBasisPoints, // taxSwapThresholdBasisPoints, todo: configurable VP demon - agentTokenTaxManager // projectTaxRecipient (fee address) - ); - await tx3.wait(); - console.log("Token params set for AgentFactoryV6"); - - // 19. Set DeployParams and LaunchParams for BondingV2 (deployer is owner initially) + // ============================================ + // 5. Set DeployParams for BondingV2 + // ============================================ console.log("\n--- Setting DeployParams for BondingV2 ---"); const deployParams = { - tbaSalt: tbaSalt, // tbaSalt - tbaImplementation: tbaImplementation, // tbaImplementation - daoVotingPeriod: daoVotingPeriod, // daoVotingPeriod - daoThreshold: daoThreshold, // daoThreshold + tbaSalt: tbaSalt, + tbaImplementation: tbaImplementation, + daoVotingPeriod: daoVotingPeriod, + daoThreshold: daoThreshold, }; - const tx4 = await bondingV2.setDeployParams(deployParams); - await tx4.wait(); - console.log("DeployParams set for BondingV2"); + await (await bondingV2.setDeployParams(deployParams)).wait(); + console.log("DeployParams set"); - // 20. Set LaunchParams for BondingV2 + // ============================================ + // 6. Set LaunchParams for BondingV2 + // ============================================ console.log("\n--- Setting LaunchParams for BondingV2 ---"); const launchParams = { - startTimeDelay: startTimeDelay, // startTimeDelay - teamTokenReservedSupply: teamTokenReservedSupply, // teamTokenReservedSupply - teamTokenReservedWallet: teamTokenReservedWallet, // teamTokenReservedWallet + startTimeDelay: startTimeDelay, + teamTokenReservedSupply: teamTokenReservedSupply, + teamTokenReservedWallet: teamTokenReservedWallet, }; - const tx5 = await bondingV2.setLaunchParams(launchParams); - await tx5.wait(); - console.log("LaunchParams set for BondingV2"); - - // 22. Grant necessary roles and Transfer ownership - console.log("\n--- Granting necessary roles, Transfer ownership ---"); - - // 22.1 Grant DEFAULT_ADMIN_ROLE of FFactoryV2 to admin - console.log("\n--- Granting DEFAULT_ADMIN_ROLE of FFactoryV2 to ADMIN ---"); - const tx6 = await fFactoryV2.grantRole( - await fFactoryV2.DEFAULT_ADMIN_ROLE(), - process.env.ADMIN - ); - await tx6.wait(); - console.log( - "Granted DEFAULT_ADMIN_ROLE of FFactoryV2 to ADMIN:", - process.env.ADMIN - ); - - // 22.2 Grant CREATOR_ROLE of FFactoryV2 to BondingV2, for createPair() - const tx10 = await fFactoryV2.grantRole( - await fFactoryV2.CREATOR_ROLE(), - bondingV2Address - ); - await tx10.wait(); + await (await bondingV2.setLaunchParams(launchParams)).wait(); + console.log("LaunchParams set"); + + // ============================================ + // 7. Grant roles (using adminSigner) + // ============================================ + console.log("\n--- Granting roles (using admin signer) ---"); + + // Grant CREATOR_ROLE of FFactoryV2 to BondingV2 + await ( + await fFactoryV2.grantRole( + await fFactoryV2.CREATOR_ROLE(), + bondingV2Address + ) + ).wait(); console.log( - "Granted CREATOR_ROLE of FFactoryV2 to BondingV2:", + "CREATOR_ROLE of FFactoryV2 granted to BondingV2:", bondingV2Address ); - // 22.3 Grant DEFAULT_ADMIN_ROLE of FRouterV2 to ADMIN - const tx6_5 = await fRouterV2.grantRole( - await fRouterV2.DEFAULT_ADMIN_ROLE(), - process.env.ADMIN - ); - await tx6_5.wait(); + // Grant EXECUTOR_ROLE of FRouterV2 to BondingV2 + await ( + await fRouterV2.grantRole( + await fRouterV2.EXECUTOR_ROLE(), + bondingV2Address + ) + ).wait(); console.log( - "Granted DEFAULT_ADMIN_ROLE of FRouterV2 to ADMIN:", - process.env.ADMIN - ); - - // 22.4 Grant EXECUTOR_ROLE of FRouterV2 to BondingV2, for buy(), sell(), addInitialLiquidity() - console.log("\n--- Granting EXECUTOR_ROLE of FRouterV2 to BondingV2 ---"); - const tx7 = await fRouterV2.grantRole( - await fRouterV2.EXECUTOR_ROLE(), + "EXECUTOR_ROLE of FRouterV2 granted to BondingV2:", bondingV2Address ); - await tx7.wait(); - console.log( - "Granted EXECUTOR_ROLE of FRouterV2 to BondingV2:", - bondingV2Address - ); - - // 22.5 Grant EXECUTOR_ROLE of FRouterV2 to BE_OPS_WALLET, for resetTime() - const tx8 = await fRouterV2.grantRole( - await fRouterV2.EXECUTOR_ROLE(), - process.env.BE_OPS_WALLET - ); - await tx8.wait(); - console.log( - "Granted EXECUTOR_ROLE of FRouterV2 to BE_OPS_WALLET:", - process.env.BE_OPS_WALLET - ); - // 22.6 Grant DEFAULT_ADMIN_ROLE of AgentFactoryV6 to ADMIN - console.log("\n--- Granting DEFAULT_ADMIN_ROLE of AgentFactoryV6 to ADMIN ---"); - const tx8_2 = await agentFactoryV6.grantRole( - await agentFactoryV6.DEFAULT_ADMIN_ROLE(), - process.env.ADMIN - ); - await tx8_2.wait(); + // Grant BONDING_ROLE of AgentFactoryV6 to BondingV2 + await ( + await agentFactoryV6.grantRole( + await agentFactoryV6.BONDING_ROLE(), + bondingV2Address + ) + ).wait(); console.log( - "Granted DEFAULT_ADMIN_ROLE of AgentFactoryV6 to ADMIN:", - process.env.ADMIN - ); - - // 22.7 Grant BONDING_ROLE of AgentFactoryV6 to BondingV2, - // for createNewAgentTokenAndApplication(), updateApplicationThresholdWithApplicationId() - // for executeBondingCurveApplicationSalt(), - // for addBlacklistAddress(), removeBlacklistAddress() - const tx9 = await agentFactoryV6.grantRole( - await agentFactoryV6.BONDING_ROLE(), - bondingV2Address - ); - await tx9.wait(); - console.log( - "Granted BONDING_ROLE of AgentFactoryV6 to BondingV2:", + "BONDING_ROLE of AgentFactoryV6 granted to BondingV2:", bondingV2Address ); - // 22.8 Grant REMOVE_LIQUIDITY_ROLE of AgentFactoryV6 to BondingV2, for removeLiquidity() - const tx9_1 = await agentFactoryV6.grantRole( - await agentFactoryV6.REMOVE_LIQUIDITY_ROLE(), - admin - ); - await tx9_1.wait(); + // ============================================ + // 8. Transfer ownership of BondingV2 + // ============================================ + console.log("\n--- Transferring BondingV2 ownership ---"); + await (await bondingV2.transferOwnership(contractController)).wait(); console.log( - "Granted REMOVE_LIQUIDITY_ROLE of AgentFactoryV6 to ADMIN:", - admin - ); - - // 22.9 Transfer ownership of BondingV2 to CONTRACT_CONTROLLER - console.log( - "\n--- Transferring BondingV2 ownership to CONTRACT_CONTROLLER ---" - ); - const tx3_5 = await bondingV2.transferOwnership( - process.env.CONTRACT_CONTROLLER - ); - await tx3_5.wait(); - console.log("BondingV2 ownership transferred to CONTRACT_CONTROLLER"); - - // 23. Revoke deployer roles (security best practice) - console.log("\n--- Revoking deployer roles ---"); - - // 23.1 Revoke deployer roles from FFactoryV2 - const tx13 = await fFactoryV2.revokeRole( - await fFactoryV2.ADMIN_ROLE(), - await deployer.getAddress() - ); - await tx13.wait(); - console.log( - "Revoked ADMIN_ROLE of FFactoryV2 from Deployer:", - await deployer.getAddress() - ); - - // 23.2 Revoke deployer roles from FRouterV2 - const tx14 = await fRouterV2.revokeRole( - await fRouterV2.ADMIN_ROLE(), - await deployer.getAddress() - ); - await tx14.wait(); - console.log( - "Revoked ADMIN_ROLE of FRouterV2 from Deployer:", - await deployer.getAddress() - ); - - // 23.3 Revoke deployer roles from AgentFactoryV6 - const tx15 = await agentFactoryV6.revokeRole( - await agentFactoryV6.DEFAULT_ADMIN_ROLE(), - await deployer.getAddress() - ); - await tx15.wait(); - console.log( - "Revoked DEFAULT_ADMIN_ROLE of AgentFactoryV6 from Deployer:", - await deployer.getAddress() - ); - - // AccessControlUpgradeable (FFactoryV2, FRouterV2): - // Automatically grants DEFAULT_ADMIN_ROLE to the deployer during deployment - - // AccessControl (AgentFactoryV6): - // Does NOT automatically grant DEFAULT_ADMIN_ROLE to anyone - - // 23.4 Revoke default admin role of ffactoryv2 from deployer - console.log("\n--- Revoking DEFAULT_ADMIN_ROLE of FFactoryV2 from Deployer ---"); - const tx16 = await fFactoryV2.revokeRole( - await fFactoryV2.DEFAULT_ADMIN_ROLE(), - await deployer.getAddress() - ); - await tx16.wait(); - console.log("Revoked DEFAULT_ADMIN_ROLE of FFactoryV2 from Deployer:", await deployer.getAddress()); - - // 23.5 Revoke default admin role of frouterv2 from deployer - console.log("\n--- Revoking DEFAULT_ADMIN_ROLE of FRouterV2 from Deployer ---"); - const tx17 = await fRouterV2.revokeRole( - await fRouterV2.DEFAULT_ADMIN_ROLE(), - await deployer.getAddress() - ); - await tx17.wait(); - console.log("Revoked DEFAULT_ADMIN_ROLE of FRouterV2 from Deployer:", await deployer.getAddress()); - - // 24. Grant MINTER_ROLE of agentNftV2 to agentFactoryV6 - // Need admin signer because only admin has DEFAULT_ADMIN_ROLE on AgentNftV2 - console.log( - "\n--- Granting MINTER_ROLE of agentNftV2 to agentFactoryV6 ---" - ); - if (!process.env.ADMIN_PRIVATE_KEY) { - console.log("⚠️ ADMIN_PRIVATE_KEY not set - skipping MINTER_ROLE grant"); - console.log("⚠️ Admin needs to manually grant MINTER_ROLE of AgentNftV2 to AgentFactoryV6:", agentFactoryV6Address); - } else { - const adminSigner = new ethers.Wallet( - process.env.ADMIN_PRIVATE_KEY, - ethers.provider - ); - const agentNftV2Contract = await ethers.getContractAt( - "AgentNftV2", - agentNftV2, - adminSigner - ); - const tx6_1 = await agentNftV2Contract.grantRole( - await agentNftV2Contract.MINTER_ROLE(), - agentFactoryV6Address - ); - await tx6_1.wait(); - console.log("✅ MINTER_ROLE of agentNftV2 granted to agentFactoryV6"); - } - - // 24. Print deployment summary - console.log("\n=== NewLaunchpad Deployment Summary ==="); - console.log("All contracts deployed and configured:"); - console.log("- AgentTokenV2 (implementation):", agentTokenV2Address); - console.log("- AgentVeTokenV2 (implementation):", agentVeTokenV2Address); - console.log("- AgentDAO (implementation):", agentDAO); - console.log("- FFactoryV2:", fFactoryV2Address); - console.log("- FRouterV2:", fRouterV2Address); - console.log("- AgentFactoryV6:", agentFactoryV6Address); - console.log("- BondingV2:", bondingV2Address); - - console.log("\nDeployment and role setup completed successfully!"); + "BondingV2 ownership transferred to CONTRACT_CONTROLLER:", + contractController + ); + + // ============================================ + // 9. Summary + // ============================================ + console.log("\n=== Deployment Summary ==="); + console.log("BondingV2:", bondingV2Address); + console.log("\nReused contracts:"); + console.log(" FFactoryV2:", fFactoryV2Address); + console.log(" FRouterV2:", fRouterV2Address); + console.log(" AgentFactoryV6:", agentFactoryV6Address); + console.log("\nRoles granted:"); + console.log(" CREATOR_ROLE of FFactoryV2 -> BondingV2"); + console.log(" EXECUTOR_ROLE of FRouterV2 -> BondingV2"); + console.log(" BONDING_ROLE of AgentFactoryV6 -> BondingV2"); + console.log("\n✅ Deployment completed successfully!"); } catch (e) { console.error("Deployment failed:", e); throw e; diff --git a/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts b/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts deleted file mode 100644 index f594ca1..0000000 --- a/scripts/launchpadv2/deployPrerequisites_ethSepolia.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { parseEther } from "ethers"; -const { ethers, upgrades } = require("hardhat"); - -(async () => { - try { - console.log("\n=== Prerequisites Deployment Starting ==="); - console.log("This script deploys: AgentDAO, AgentNftV2, AgentTax (AGENT_TAX_CONTRACT_ADDRESS)"); - - // Basic check for .env variables - const deployerAddress = process.env.DEPLOYER; - if (!deployerAddress) { - throw new Error("DEPLOYER not set in environment"); - } - const contractController = process.env.CONTRACT_CONTROLLER; - if (!contractController) { - throw new Error("CONTRACT_CONTROLLER not set in environment"); - } - const admin = process.env.ADMIN; - if (!admin) { - throw new Error("ADMIN not set in environment"); - } - - // Required for AgentTax - const assetToken = process.env.BRIDGED_TOKEN; - if (!assetToken) { - throw new Error("BRIDGED_TOKEN not set in environment"); - } - const taxToken = process.env.AGENT_TAX_TOKEN; - if (!taxToken) { - throw new Error("AGENT_TAX_TOKEN not set in environment"); - } - const uniswapV2Router = process.env.UNISWAP_V2_ROUTER; - if (!uniswapV2Router) { - throw new Error("UNISWAP_V2_ROUTER not set in environment"); - } - const treasury = process.env.AGENT_TAX_TREASURY; - if (!treasury) { - throw new Error("AGENT_TAX_TREASURY not set in environment"); - } - const minSwapThreshold = process.env.TAX_MANAGER_MIN_SWAP_THRESHOLD; - if (!minSwapThreshold) { - throw new Error("TAX_MANAGER_MIN_SWAP_THRESHOLD not set in environment"); - } - const maxSwapThreshold = process.env.TAX_MANAGER_MAX_SWAP_THRESHOLD; - if (!maxSwapThreshold) { - throw new Error("TAX_MANAGER_MAX_SWAP_THRESHOLD not set in environment"); - } - - console.log("\nDeployment arguments loaded:", { - deployerAddress, - contractController, - admin, - assetToken, - taxToken, - uniswapV2Router, - treasury, - minSwapThreshold, - maxSwapThreshold, - }); - - const signers = await ethers.getSigners(); - const deployer = signers[0]; - console.log("Deployer address:", await deployer.getAddress()); - - // ============================================ - // 1. Deploy AgentDAO (implementation contract, not proxy) - // ============================================ - // console.log("\n--- Deploying AgentDAO (implementation) ---"); - // const AgentDAO = await ethers.getContractFactory("AgentDAO"); - // const agentDAO = await AgentDAO.deploy(); - // await agentDAO.waitForDeployment(); - // const agentDAOAddress = await agentDAO.getAddress(); - // console.log("AgentDAO (implementation) deployed at:", agentDAOAddress); - - // ============================================ - // 2. Deploy AgentNftV2 (upgradeable proxy) - // ============================================ - // console.log("\n--- Deploying AgentNftV2 (proxy) ---"); - // const AgentNftV2 = await ethers.getContractFactory("AgentNftV2"); - // const agentNftV2 = await upgrades.deployProxy( - // AgentNftV2, - // [admin], // initialize(address defaultAdmin) - // { - // initializer: "initialize", - // initialOwner: contractController, - // unsafeAllow: ["internal-function-storage"], - // } - // ); - // await agentNftV2.waitForDeployment(); - // const agentNftV2Address = await agentNftV2.getAddress(); - // console.log("AgentNftV2 (proxy) deployed at:", agentNftV2Address); - // Note: initialize() already grants DEFAULT_ADMIN_ROLE, VALIDATOR_ADMIN_ROLE, ADMIN_ROLE to admin - // MINTER_ROLE will be granted to AgentFactoryV6 in the main deployment script - - // ============================================ - // 3. Deploy AgentTax (AGENT_TAX_CONTRACT_ADDRESS) - // ============================================ - const agentNftV2Address = process.env.AGENT_NFT_V2; - - console.log("\n--- Deploying AgentTax (AGENT_TAX_CONTRACT_ADDRESS) ---"); - const AgentTax = await ethers.getContractFactory("AgentTax"); - const agentTax = await upgrades.deployProxy( - AgentTax, - [ - admin, // defaultAdmin_ - assetToken, // assetToken_ (BRIDGED_TOKEN/VIRTUAL) - taxToken, // taxToken_ - uniswapV2Router, // router_ - treasury, // treasury_ - parseEther(minSwapThreshold), // minSwapThreshold_ - parseEther(maxSwapThreshold), // maxSwapThreshold_ - agentNftV2Address, // nft_ (AgentNftV2) - ], - { - initializer: "initialize", - initialOwner: contractController, - } - ); - await agentTax.waitForDeployment(); - const agentTaxAddress = await agentTax.getAddress(); - console.log("AgentTax deployed at:", agentTaxAddress); - // Note: initialize() grants ADMIN_ROLE and DEFAULT_ADMIN_ROLE to admin - // Admin needs to manually grant EXECUTOR_ROLE and EXECUTOR_V2_ROLE to the executor address - - // ============================================ - // 4. Print Deployment Summary - // ============================================ - console.log("\n=== Prerequisites Deployment Summary ==="); - console.log("Copy the following addresses to your .env file:\n"); - // console.log(`AGENT_DAO=${agentDAOAddress}`); - console.log(`AGENT_NFT_V2=${agentNftV2Address}`); - console.log(`AGENT_TAX_CONTRACT_ADDRESS=${agentTaxAddress}`); - - console.log("\n--- Full Summary ---"); - // console.log("- AgentDAO (implementation):", agentDAOAddress); - console.log("- AgentNftV2 (proxy):", agentNftV2Address); - console.log("- AgentTax (AGENT_TAX_CONTRACT_ADDRESS):", agentTaxAddress); - - console.log("\n--- Manual Steps Required (by admin) ---"); - console.log("AgentNftV2: MINTER_ROLE will be granted to AgentFactoryV6 in main deployment script"); - console.log("AgentTax: Admin needs to grant EXECUTOR_ROLE and EXECUTOR_V2_ROLE to executor address"); - - console.log("\nPrerequisites deployment completed successfully!"); - } catch (e) { - console.error("Deployment failed:", e); - throw e; - } -})(); diff --git a/scripts/launchpadv2/e2e_test_bondingv2.ts b/scripts/launchpadv2/e2e_test_bondingv2.ts new file mode 100644 index 0000000..b6ceaf5 --- /dev/null +++ b/scripts/launchpadv2/e2e_test_bondingv2.ts @@ -0,0 +1,847 @@ +import { parseEther, formatEther } from "ethers"; +const { ethers } = require("hardhat"); + +/** + * BondingV2 E2E Test Script + * + * Tests the full lifecycle: preLaunch -> launch -> buy/sell with anti-sniper tax verification + * + * Key differences from BondingV5: + * - No BondingConfig contract (all params in BondingV2) + * - preLaunch requires startTime >= block.timestamp + startTimeDelay (no immediate launch) + * - Simpler preLaunch signature (no launchMode, airdropBips, needAcf, antiSniperTaxType) + * - Anti-sniper duration: (antiSniperBuyTaxStartValue / 100) * 60 minutes + * e.g., antiSniperBuyTaxStartValue=5 means 5 minutes anti-sniper period + * + * Usage: + * npx hardhat run scripts/launchpadv2/e2e_test_bondingv2.ts --network eth_sepolia + */ + +/** + * Wait for a specified number of seconds with progress indicator + */ +async function waitWithProgress(seconds: number, message: string): Promise { + console.log(`\n⏳ ${message}`); + console.log(` Waiting ${seconds} seconds...`); + + const startTime = Date.now(); + const endTime = startTime + seconds * 1000; + + const progressInterval = Math.max(10, Math.floor(seconds / 10)); + let lastProgress = 0; + + while (Date.now() < endTime) { + const elapsed = Math.floor((Date.now() - startTime) / 1000); + const remaining = seconds - elapsed; + + if (elapsed - lastProgress >= progressInterval || remaining <= 5) { + console.log(` ⏱️ ${elapsed}s elapsed, ${remaining}s remaining...`); + lastProgress = elapsed; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + console.log(` ✅ Wait complete!`); +} + +interface TestConfig { + bondingV2Address: string; + fFactoryV2Address: string; + fRouterV2Address: string; + virtualTokenAddress: string; + agentFactoryV6Address: string; +} + +async function main() { + console.log("\n" + "=".repeat(80)); + console.log(" BondingV2 E2E Test - Comprehensive Verification"); + console.log("=".repeat(80)); + + // Load contract addresses from environment + const config: TestConfig = { + bondingV2Address: process.env.BONDING_V2_ADDRESS || "", + fFactoryV2Address: process.env.FFactoryV2_ADDRESS || "", + fRouterV2Address: process.env.FRouterV2_ADDRESS || "", + virtualTokenAddress: process.env.VIRTUAL_TOKEN_ADDRESS || "", + agentFactoryV6Address: process.env.AGENT_FACTORY_V6_ADDRESS || "", + }; + + // Validate required addresses + for (const [key, value] of Object.entries(config)) { + if (!value) { + throw new Error(`${key} not set in environment`); + } + } + + console.log("\n--- Contract Addresses ---"); + console.log("BondingV2:", config.bondingV2Address); + console.log("FFactoryV2:", config.fFactoryV2Address); + console.log("FRouterV2:", config.fRouterV2Address); + console.log("VIRTUAL Token:", config.virtualTokenAddress); + console.log("AgentFactoryV6:", config.agentFactoryV6Address); + + // Get signer + const [signer] = await ethers.getSigners(); + const signerAddress = await signer.getAddress(); + console.log("\n--- Signer ---"); + console.log("Address:", signerAddress); + + // Get contract instances + const bondingV2 = await ethers.getContractAt("BondingV2", config.bondingV2Address); + const fFactoryV2 = await ethers.getContractAt("FFactoryV2", config.fFactoryV2Address); + const fRouterV2 = await ethers.getContractAt("FRouterV2", config.fRouterV2Address); + const virtualToken = await ethers.getContractAt("IERC20", config.virtualTokenAddress); + const agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", config.agentFactoryV6Address); + + // ============================================ + // Step 1: Verify Configuration Parameters + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 1: Verify Configuration Parameters"); + console.log("=".repeat(80)); + + // BondingV2 params (no BondingConfig) + const initialSupply = await bondingV2.initialSupply(); + const fee = await bondingV2.fee(); + const assetRate = await bondingV2.assetRate(); + const gradThreshold = await bondingV2.gradThreshold(); + const maxTx = await bondingV2.maxTx(); + const launchParams = await bondingV2.launchParams(); + + console.log("\n--- BondingV2 Parameters ---"); + console.log("Initial Supply:", initialSupply.toString()); + console.log("Fee:", formatEther(fee), "VIRTUAL"); + console.log("Asset Rate:", assetRate.toString()); + console.log("Grad Threshold:", formatEther(gradThreshold), "tokens"); + console.log("Max Tx:", maxTx.toString()); + console.log("\n--- LaunchParams ---"); + console.log("Start Time Delay:", launchParams.startTimeDelay.toString(), "seconds"); + console.log("Team Token Reserved Supply:", launchParams.teamTokenReservedSupply.toString()); + console.log("Team Token Reserved Wallet:", launchParams.teamTokenReservedWallet); + + // FFactoryV2 tax parameters + const buyTax = await fFactoryV2.buyTax(); + const sellTax = await fFactoryV2.sellTax(); + const antiSniperBuyTaxStartValue = await fFactoryV2.antiSniperBuyTaxStartValue(); + const taxVault = await fFactoryV2.taxVault(); + const antiSniperTaxVault = await fFactoryV2.antiSniperTaxVault(); + + console.log("\n--- FFactoryV2 Tax Parameters ---"); + console.log("Buy Tax:", buyTax.toString(), "%"); + console.log("Sell Tax:", sellTax.toString(), "%"); + console.log("Anti-Sniper Buy Tax Start Value:", antiSniperBuyTaxStartValue.toString(), "%"); + console.log("Tax Vault:", taxVault); + console.log("Anti-Sniper Tax Vault:", antiSniperTaxVault); + + // Calculate anti-sniper duration for BondingV2 + // Formula: (antiSniperBuyTaxStartValue / 100) * 60 minutes = antiSniperBuyTaxStartValue * 60 seconds + // e.g., antiSniperBuyTaxStartValue=5 means 5 minutes = 300 seconds + const antiSniperDurationSeconds = Number(antiSniperBuyTaxStartValue) * 60; + console.log("\n--- Anti-Sniper Tax Duration (calculated) ---"); + console.log("Duration:", antiSniperDurationSeconds, "seconds (", antiSniperBuyTaxStartValue.toString(), " minutes)"); + + // ============================================ + // Step 2: Check Virtual Token Balance and Approve + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 2: Check VIRTUAL Token Balance and Approve"); + console.log("=".repeat(80)); + + const virtualBalance = await virtualToken.balanceOf(signerAddress); + console.log("VIRTUAL Balance:", formatEther(virtualBalance), "VIRTUAL"); + + const requiredAllowance = parseEther("10000"); + + // Approve to BondingV2 + const bondingAllowance = await virtualToken.allowance(signerAddress, config.bondingV2Address); + console.log("\n--- Checking BondingV2 Allowance ---"); + console.log("Current Allowance:", formatEther(bondingAllowance), "VIRTUAL"); + + if (BigInt(bondingAllowance) < BigInt(requiredAllowance)) { + console.log("--- Approving VIRTUAL tokens to BondingV2 ---"); + const approveTx = await virtualToken.approve(config.bondingV2Address, requiredAllowance); + await approveTx.wait(); + console.log("✅ Approved", formatEther(requiredAllowance), "VIRTUAL to BondingV2"); + } else { + console.log("✅ Already approved sufficient VIRTUAL tokens to BondingV2"); + } + + // Approve to FRouterV2 + const routerAllowance = await virtualToken.allowance(signerAddress, config.fRouterV2Address); + console.log("\n--- Checking FRouterV2 Allowance ---"); + console.log("Current Router Allowance:", formatEther(routerAllowance), "VIRTUAL"); + + if (BigInt(routerAllowance) < BigInt(requiredAllowance)) { + console.log("--- Approving VIRTUAL tokens to FRouterV2 ---"); + const approveTx = await virtualToken.approve(config.fRouterV2Address, requiredAllowance); + await approveTx.wait(); + console.log("✅ Approved", formatEther(requiredAllowance), "VIRTUAL to FRouterV2"); + } else { + console.log("✅ Already approved sufficient VIRTUAL tokens to FRouterV2"); + } + + // ============================================ + // Step 3: Verify BondingV2 Roles + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 3: Verify BondingV2 Roles"); + console.log("=".repeat(80)); + + const bondingRole = await agentFactoryV6.BONDING_ROLE(); + const hasBondingRole = await agentFactoryV6.hasRole(bondingRole, config.bondingV2Address); + console.log("BondingV2 has BONDING_ROLE on AgentFactoryV6:", hasBondingRole); + if (!hasBondingRole) { + throw new Error("BondingV2 does not have BONDING_ROLE on AgentFactoryV6!"); + } + + const creatorRole = await fFactoryV2.CREATOR_ROLE(); + const hasCreatorRole = await fFactoryV2.hasRole(creatorRole, config.bondingV2Address); + console.log("BondingV2 has CREATOR_ROLE on FFactoryV2:", hasCreatorRole); + if (!hasCreatorRole) { + throw new Error("BondingV2 does not have CREATOR_ROLE on FFactoryV2!"); + } + + const executorRole = await fRouterV2.EXECUTOR_ROLE(); + const hasExecutorRole = await fRouterV2.hasRole(executorRole, config.bondingV2Address); + console.log("BondingV2 has EXECUTOR_ROLE on FRouterV2:", hasExecutorRole); + if (!hasExecutorRole) { + throw new Error("BondingV2 does not have EXECUTOR_ROLE on FRouterV2!"); + } + + // ============================================ + // Step 4: Test preLaunch + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 4: Test preLaunch"); + console.log("=".repeat(80)); + + const tokenName = `E2E V2 Test ${Date.now()}`; + const tokenTicker = `V2T${Math.floor(Math.random() * 1000)}`; + const cores = [0, 1, 2, 4]; + const description = "E2E Test Token for BondingV2"; + const image = "https://example.com/e2e-test.png"; + const urls: [string, string, string, string] = ["", "", "", ""]; + const purchaseAmount = parseEther("200"); // 200 VIRTUAL (must be > fee) + + // BondingV2 requires startTime >= block.timestamp + startTimeDelay + const latestBlock = await ethers.provider.getBlock("latest"); + const currentTimestamp = Number(latestBlock!.timestamp); + const startTimeDelayNum = Number(launchParams.startTimeDelay); + // Set startTime to be startTimeDelay + 5 seconds from now + const startTime = currentTimestamp + startTimeDelayNum + 30; + + console.log("\n--- preLaunch Parameters ---"); + console.log("Token Name:", tokenName); + console.log("Token Ticker:", tokenTicker); + console.log("Cores:", cores); + console.log("Purchase Amount:", formatEther(purchaseAmount), "VIRTUAL"); + console.log("Fee:", formatEther(fee), "VIRTUAL"); + console.log("Initial Purchase (after fee):", formatEther(purchaseAmount - fee), "VIRTUAL"); + console.log("Current Block Timestamp:", currentTimestamp); + console.log("Start Time Delay:", startTimeDelayNum, "seconds"); + console.log("Start Time:", startTime, `(${new Date(startTime * 1000).toISOString()})`); + + // Get feeTo balance before preLaunch + const feeTo = await bondingV2._feeTo ? await bondingV2._feeTo() : (await bondingV2.owner()); // Fallback + let feeToAddress: string; + try { + // Try to get _feeTo through a view function or storage + const bondingV2Abi = [ + "function _feeTo() view returns (address)", + ]; + const bondingV2WithFeeTo = new ethers.Contract(config.bondingV2Address, bondingV2Abi, signer); + feeToAddress = await bondingV2WithFeeTo._feeTo(); + } catch { + // _feeTo might be private, use the one from env or launchParams + feeToAddress = process.env.LAUNCHPAD_V5_CREATION_FEE_TO_ADDRESS || launchParams.teamTokenReservedWallet; + } + console.log("Fee To Address:", feeToAddress); + const feeToBalanceBefore = await virtualToken.balanceOf(feeToAddress); + + console.log("\n--- Executing preLaunch ---"); + + // Test with staticCall first + try { + console.log("--- Running staticCall to check for errors ---"); + await bondingV2.preLaunch.staticCall( + tokenName, + tokenTicker, + cores, + description, + image, + urls, + purchaseAmount, + startTime + ); + console.log("✅ staticCall passed, proceeding with actual transaction..."); + } catch (staticCallError: any) { + console.error("\n❌ staticCall failed:"); + console.error("Error:", staticCallError.message); + if (staticCallError.reason) { + console.error("Reason:", staticCallError.reason); + } + throw staticCallError; + } + + // Estimate gas + const estimatedGas = await bondingV2.preLaunch.estimateGas( + tokenName, + tokenTicker, + cores, + description, + image, + urls, + purchaseAmount, + startTime + ); + console.log("Estimated Gas:", estimatedGas.toString()); + + const gasLimit = (estimatedGas * 150n) / 100n; + console.log("Using Gas Limit:", gasLimit.toString()); + + const preLaunchTx = await bondingV2.preLaunch( + tokenName, + tokenTicker, + cores, + description, + image, + urls, + purchaseAmount, + startTime, + ); + + const preLaunchReceipt = await preLaunchTx.wait(); + console.log("✅ preLaunch transaction successful!"); + console.log("Gas Used:", preLaunchReceipt.gasUsed.toString()); + + // Parse PreLaunched event + const preLaunchedEvent = preLaunchReceipt.logs.find((log: any) => { + try { + const parsed = bondingV2.interface.parseLog(log); + return parsed?.name === "PreLaunched"; + } catch { + return false; + } + }); + + if (!preLaunchedEvent) { + throw new Error("PreLaunched event not found"); + } + + const parsedEvent = bondingV2.interface.parseLog(preLaunchedEvent); + const tokenAddress = parsedEvent.args[0]; // token + const pairAddress = parsedEvent.args[1]; // pair + const virtualId = parsedEvent.args[2]; // virtualId + const initialPurchase = parsedEvent.args[3]; // initialPurchase + + console.log("\n--- PreLaunched Event Data ---"); + console.log("Token Address:", tokenAddress); + console.log("Pair Address:", pairAddress); + console.log("Virtual ID:", virtualId.toString()); + console.log("Initial Purchase:", formatEther(initialPurchase), "VIRTUAL"); + + // ============================================ + // Step 5: Verify On-Chain Parameters + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 5: Verify On-Chain Parameters"); + console.log("=".repeat(80)); + + const tokenInfo = await bondingV2.tokenInfo(tokenAddress); + console.log("\n--- tokenInfo ---"); + console.log("Creator:", tokenInfo.creator); + console.log("Token:", tokenInfo.token); + console.log("Pair:", tokenInfo.pair); + console.log("Description:", tokenInfo.description); + console.log("Trading:", tokenInfo.trading); + console.log("Trading on Uniswap:", tokenInfo.tradingOnUniswap); + console.log("Launch Executed:", tokenInfo.launchExecuted); + console.log("Initial Purchase:", formatEther(tokenInfo.initialPurchase), "VIRTUAL"); + console.log("Application ID:", tokenInfo.applicationId.toString()); + + // Verify fee collection + const feeToBalanceAfter = await virtualToken.balanceOf(feeToAddress); + const feeCollected = feeToBalanceAfter - feeToBalanceBefore; + console.log("\n--- Fee Verification ---"); + console.log("Fee To Balance Before:", formatEther(feeToBalanceBefore), "VIRTUAL"); + console.log("Fee To Balance After:", formatEther(feeToBalanceAfter), "VIRTUAL"); + console.log("Fee Collected:", formatEther(feeCollected), "VIRTUAL"); + console.log("Expected Fee:", formatEther(fee), "VIRTUAL"); + + if (BigInt(feeCollected) === BigInt(fee)) { + console.log("✅ Fee collection correct!"); + } else { + console.log("⚠️ Fee mismatch"); + } + + // ============================================ + // Step 6: Wait for Start Time and Launch + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 6: Wait for Start Time and Launch"); + console.log("=".repeat(80)); + + const pair = await ethers.getContractAt("IFPairV2", pairAddress); + const pairStartTime = await pair.startTime(); + console.log("Pair Start Time:", new Date(Number(pairStartTime) * 1000).toISOString()); + + const currentTime = Math.floor(Date.now() / 1000); + const waitTime = Number(pairStartTime) - currentTime; + + if (waitTime > 0) { + await waitWithProgress(waitTime + 2, "Waiting for pair start time to be reached..."); + } else { + console.log("✅ Start time already passed, can proceed with launch"); + } + + console.log("\n--- Executing launch ---"); + const launchTx = await bondingV2.launch(tokenAddress); + const launchReceipt = await launchTx.wait(); + console.log("✅ launch() transaction successful!"); + console.log("Gas Used:", launchReceipt.gasUsed.toString()); + + // Parse Launched event + const launchedEvent = launchReceipt.logs.find((log: any) => { + try { + const parsed = bondingV2.interface.parseLog(log); + return parsed?.name === "Launched"; + } catch { + return false; + } + }); + + if (launchedEvent) { + const parsedLaunchedEvent = bondingV2.interface.parseLog(launchedEvent); + console.log("\n--- Launched Event Data ---"); + console.log("Initial Purchase Amount:", formatEther(parsedLaunchedEvent.args[3]), "VIRTUAL"); + console.log("Initial Purchased Amount:", formatEther(parsedLaunchedEvent.args[4]), "tokens"); + } + + // Verify token status after launch + const tokenInfoAfterLaunch = await bondingV2.tokenInfo(tokenAddress); + console.log("\n--- Token Status After Launch ---"); + console.log("Launch Executed:", tokenInfoAfterLaunch.launchExecuted); + console.log("Trading:", tokenInfoAfterLaunch.trading); + + // ============================================ + // Step 7: Test Buy with Anti-Sniper Tax Verification + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 7: Test Buy with Anti-Sniper Tax Verification"); + console.log("=".repeat(80)); + + const agentToken = await ethers.getContractAt("IERC20", tokenAddress); + + // Check anti-sniper status + const hasAntiSniperTax = await fRouterV2.hasAntiSniperTax(pairAddress); + const taxStartTime = await pair.taxStartTime(); + const currentBlockTime = (await ethers.provider.getBlock("latest")).timestamp; + const timeSinceLaunch = currentBlockTime - Number(taxStartTime); + + console.log("\n--- Anti-Sniper Tax Status ---"); + console.log("Tax Start Time:", new Date(Number(taxStartTime) * 1000).toISOString()); + console.log("Current Block Time:", new Date(currentBlockTime * 1000).toISOString()); + console.log("Time Since Launch:", timeSinceLaunch, "seconds"); + console.log("Anti-Sniper Duration:", antiSniperDurationSeconds, "seconds"); + console.log("Has Anti-Sniper Tax Active:", hasAntiSniperTax); + + // Get tax vault balances before buy + const taxVaultBalanceBefore = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceBefore = await virtualToken.balanceOf(antiSniperTaxVault); + + console.log("\n--- Tax Vault Balances Before Buy ---"); + console.log("Tax Vault Balance:", formatEther(taxVaultBalanceBefore), "VIRTUAL"); + console.log("Anti-Sniper Tax Vault Balance:", formatEther(antiSniperTaxVaultBalanceBefore), "VIRTUAL"); + + const buyAmount = parseEther("100"); + const deadline = Math.floor(Date.now() / 1000) + 300; + + const agentTokenBalanceBefore = await agentToken.balanceOf(signerAddress); + + console.log("\n--- Buy Parameters ---"); + console.log("Buy Amount:", formatEther(buyAmount), "VIRTUAL"); + console.log("Agent Token Balance Before:", formatEther(agentTokenBalanceBefore), "tokens"); + + console.log("\n--- Executing buy (during anti-sniper period) ---"); + const buyTx = await bondingV2.buy(buyAmount, tokenAddress, 0, deadline); + const buyReceipt = await buyTx.wait(); + console.log("✅ buy() transaction successful!"); + console.log("Gas Used:", buyReceipt.gasUsed.toString()); + + const agentTokenBalanceAfterBuy = await agentToken.balanceOf(signerAddress); + const tokensReceived = agentTokenBalanceAfterBuy - agentTokenBalanceBefore; + console.log("Agent Token Balance After:", formatEther(agentTokenBalanceAfterBuy), "tokens"); + console.log("Tokens Received:", formatEther(tokensReceived), "tokens"); + + // Get tax vault balances after buy + const taxVaultBalanceAfterBuy = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceAfterBuy = await virtualToken.balanceOf(antiSniperTaxVault); + + const normalTaxCollected = taxVaultBalanceAfterBuy - taxVaultBalanceBefore; + const antiSniperTaxCollected = antiSniperTaxVaultBalanceAfterBuy - antiSniperTaxVaultBalanceBefore; + + console.log("\n--- Tax Collected ---"); + console.log("Normal Tax Collected:", formatEther(normalTaxCollected), "VIRTUAL"); + console.log("Anti-Sniper Tax Collected:", formatEther(antiSniperTaxCollected), "VIRTUAL"); + + if (hasAntiSniperTax && BigInt(antiSniperTaxCollected) > 0n) { + console.log("✅ Anti-Sniper Tax correctly collected to antiSniperTaxVault"); + } else if (BigInt(normalTaxCollected) > 0n) { + console.log("✅ Normal Tax collected to taxVault"); + } + + // ============================================ + // Step 8: Wait for Anti-Sniper Period to End and Test Buy + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 8: Wait for Anti-Sniper Period to End and Test Buy"); + console.log("=".repeat(80)); + + const hasAntiSniperTaxNow = await fRouterV2.hasAntiSniperTax(pairAddress); + if (hasAntiSniperTaxNow) { + const currentTime2 = (await ethers.provider.getBlock("latest")).timestamp; + const remainingAntiSniperTime = Number(taxStartTime) + antiSniperDurationSeconds - currentTime2; + + if (remainingAntiSniperTime > 0) { + await waitWithProgress( + remainingAntiSniperTime + 5, + `Waiting for anti-sniper period to end (${antiSniperDurationSeconds} seconds total)...` + ); + } else { + console.log("✅ Anti-sniper period already ended"); + } + } else { + console.log("✅ No anti-sniper tax was active"); + } + + // Verify anti-sniper tax is no longer active + const hasAntiSniperTaxAfterWait = await fRouterV2.hasAntiSniperTax(pairAddress); + console.log("\nAnti-Sniper Tax Active After Wait:", hasAntiSniperTaxAfterWait); + + // Get tax vault balances before second buy + const taxVaultBalanceBeforeBuy2 = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceBeforeBuy2 = await virtualToken.balanceOf(antiSniperTaxVault); + + const buyAmount2 = parseEther("50"); + const deadline2 = Math.floor(Date.now() / 1000) + 300; + + console.log("\n--- Executing buy (after anti-sniper period) ---"); + const buyTx2 = await bondingV2.buy(buyAmount2, tokenAddress, 0, deadline2); + const buyReceipt2 = await buyTx2.wait(); + console.log("✅ buy() transaction successful!"); + console.log("Gas Used:", buyReceipt2.gasUsed.toString()); + + // Get tax vault balances after second buy + const taxVaultBalanceAfterBuy2 = await virtualToken.balanceOf(taxVault); + const antiSniperTaxVaultBalanceAfterBuy2 = await virtualToken.balanceOf(antiSniperTaxVault); + + const normalTaxCollected2 = taxVaultBalanceAfterBuy2 - taxVaultBalanceBeforeBuy2; + const antiSniperTaxCollected2 = antiSniperTaxVaultBalanceAfterBuy2 - antiSniperTaxVaultBalanceBeforeBuy2; + + console.log("\n--- Tax Collected After Anti-Sniper Period ---"); + console.log("Normal Tax Collected:", formatEther(normalTaxCollected2), "VIRTUAL"); + console.log("Anti-Sniper Tax Collected:", formatEther(antiSniperTaxCollected2), "VIRTUAL"); + + if (!hasAntiSniperTaxAfterWait && BigInt(normalTaxCollected2) > 0n && BigInt(antiSniperTaxCollected2) === 0n) { + console.log("✅ After anti-sniper period: Only normal tax collected (no anti-sniper tax)"); + } + + // ============================================ + // Step 9: Buy to Graduation + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 9: Buy to Graduation"); + console.log("=".repeat(80)); + + // Get current pair reserves to calculate how much we need to buy to trigger graduation + const pairForGrad = await ethers.getContractAt("IFPairV2", pairAddress); + const [reserveToken, reserveAsset] = await pairForGrad.getReserves(); + const tokenBalance = await pairForGrad.balance(); + const assetBalance = await pairForGrad.assetBalance(); + + console.log("\n--- Current Pair State ---"); + console.log("Token Reserve (reserveA):", formatEther(reserveToken), "tokens"); + console.log("Asset Reserve (reserveB):", formatEther(reserveAsset), "VIRTUAL"); + console.log("Token Balance:", formatEther(tokenBalance), "tokens"); + console.log("Asset Balance:", formatEther(assetBalance), "VIRTUAL"); + console.log("Graduation Threshold:", formatEther(gradThreshold), "tokens"); + + // Graduation happens when: newReserveA <= gradThreshold + // We need to buy enough to reduce reserveToken to gradThreshold or below + const tokensNeededToBuy = BigInt(reserveToken) - BigInt(gradThreshold); + console.log("\n--- Graduation Calculation ---"); + console.log("Tokens needed to buy for graduation:", formatEther(tokensNeededToBuy > 0n ? tokensNeededToBuy : 0n), "tokens"); + + if (tokensNeededToBuy <= 0n) { + console.log("⚠️ Token reserve already at or below graduation threshold, graduation should trigger on next buy"); + } + + // Check current token status + const tokenInfoBeforeGrad = await bondingV2.tokenInfo(tokenAddress); + console.log("\n--- Token Status Before Graduation ---"); + console.log("Trading:", tokenInfoBeforeGrad.trading); + console.log("Trading on Uniswap:", tokenInfoBeforeGrad.tradingOnUniswap); + console.log("Agent Token:", tokenInfoBeforeGrad.agentToken); + console.log("Application ID:", tokenInfoBeforeGrad.applicationId.toString()); + + // We'll buy in larger chunks until graduation triggers + // Graduation requires: newReserveA <= gradThreshold AND !hasAntiSniperTax AND trading + console.log("\n--- Buying to trigger graduation ---"); + + let graduated = false; + let buyCount = 0; + const maxBuys = 50; // Safety limit + const buyChunkSize = parseEther("20000"); // Buy 500 VIRTUAL per transaction + + // Check we have enough VIRTUAL balance + const currentVirtualBalance = await virtualToken.balanceOf(signerAddress); + console.log("Current VIRTUAL Balance:", formatEther(currentVirtualBalance), "VIRTUAL"); + + // Ensure we have sufficient allowance for multiple buys + const totalRequiredAllowance = buyChunkSize * BigInt(maxBuys); + const currentBondingAllowance = await virtualToken.allowance(signerAddress, config.bondingV2Address); + if (BigInt(currentBondingAllowance) < totalRequiredAllowance) { + console.log("--- Increasing VIRTUAL allowance for graduation buys ---"); + const approveMoreTx = await virtualToken.approve(config.bondingV2Address, totalRequiredAllowance); + await approveMoreTx.wait(); + console.log("✅ Approved additional VIRTUAL to BondingV2"); + } + + while (!graduated && buyCount < maxBuys) { + // Check if token is still trading (graduation hasn't happened) + const currentTokenInfo = await bondingV2.tokenInfo(tokenAddress); + if (!currentTokenInfo.trading || currentTokenInfo.tradingOnUniswap) { + console.log("\n✅ Token has graduated! (trading=false, tradingOnUniswap=true)"); + graduated = true; + break; + } + + // Get current reserves + const [currentReserveToken] = await pairForGrad.getReserves(); + const tokensRemaining = BigInt(currentReserveToken) - BigInt(gradThreshold); + + buyCount++; + console.log(`\n--- Buy #${buyCount} ---`); + console.log("Current Token Reserve:", formatEther(currentReserveToken), "tokens"); + console.log("Tokens remaining until threshold:", formatEther(tokensRemaining > 0n ? tokensRemaining : 0n), "tokens"); + + // Determine buy amount - use smaller amount if we're close to graduation + let actualBuyAmount = buyChunkSize; + if (tokensRemaining <= 0n) { + // We're at or past threshold, a small buy should trigger graduation + actualBuyAmount = parseEther("10"); + console.log("Close to graduation, using smaller buy amount:", formatEther(actualBuyAmount), "VIRTUAL"); + } + + try { + const gradBuyDeadline = Math.floor(Date.now() / 1000) + 300; + const gradBuyTx = await bondingV2.buy(actualBuyAmount, tokenAddress, 0, gradBuyDeadline); + const gradBuyReceipt = await gradBuyTx.wait(); + console.log("Buy successful, gas used:", gradBuyReceipt.gasUsed.toString()); + + // Check for Graduated event + const graduatedEvent = gradBuyReceipt.logs.find((log: any) => { + try { + const parsed = bondingV2.interface.parseLog(log); + return parsed?.name === "Graduated"; + } catch { + return false; + } + }); + + if (graduatedEvent) { + const parsedGradEvent = bondingV2.interface.parseLog(graduatedEvent); + console.log("\n🎉 GRADUATION EVENT DETECTED!"); + console.log("Token Address:", parsedGradEvent.args[0]); + console.log("Agent Token:", parsedGradEvent.args[1]); + graduated = true; + } + } catch (buyError: any) { + console.error("Buy failed:", buyError.message); + // Check if graduation happened anyway + const checkTokenInfo = await bondingV2.tokenInfo(tokenAddress); + if (!checkTokenInfo.trading || checkTokenInfo.tradingOnUniswap) { + console.log("✅ Token has graduated despite error!"); + graduated = true; + } else { + throw buyError; + } + } + } + + if (!graduated) { + console.log(`\n⚠️ Graduation not triggered after ${maxBuys} buys`); + console.log("This may indicate insufficient VIRTUAL balance or incorrect threshold configuration"); + } + + // ============================================ + // Step 10: Verify Graduation State + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 10: Verify Graduation State"); + console.log("=".repeat(80)); + + const tokenInfoAfterGrad = await bondingV2.tokenInfo(tokenAddress); + console.log("\n--- Token Status After Graduation ---"); + console.log("Trading:", tokenInfoAfterGrad.trading, "(expected: false)"); + console.log("Trading on Uniswap:", tokenInfoAfterGrad.tradingOnUniswap, "(expected: true)"); + console.log("Agent Token:", tokenInfoAfterGrad.agentToken); + + if (!tokenInfoAfterGrad.trading && tokenInfoAfterGrad.tradingOnUniswap) { + console.log("\n✅ Token correctly graduated!"); + + // Verify AgentToken was created + if (tokenInfoAfterGrad.agentToken !== ethers.ZeroAddress) { + console.log("✅ AgentToken was created:", tokenInfoAfterGrad.agentToken); + + // Check if the agentToken has the expected properties + try { + const createdAgentToken = await ethers.getContractAt("IERC20", tokenInfoAfterGrad.agentToken); + const agentTokenTotalSupply = await createdAgentToken.totalSupply(); + console.log("AgentToken Total Supply:", formatEther(agentTokenTotalSupply)); + } catch (e) { + console.log("Note: Could not query AgentToken details"); + } + } else { + console.log("⚠️ AgentToken address is zero - check executeBondingCurveApplicationSalt"); + } + + // Verify application was executed in AgentFactoryV6 + try { + const application = await agentFactoryV6.getApplication(tokenInfoAfterGrad.applicationId); + console.log("\n--- Application Status in AgentFactoryV6 ---"); + console.log("Application ID:", tokenInfoAfterGrad.applicationId.toString()); + console.log("Application Status:", application.status, "(1 = Executed)"); + console.log("Application Virtual ID:", application.virtualId.toString()); + console.log("Application Token Address:", application.tokenAddress); + + if (application.status === 1n || application.status === 1) { + console.log("✅ Application correctly marked as Executed in AgentFactoryV6"); + } + } catch (e: any) { + console.log("Note: Could not query application details:", e.message); + } + } else { + console.log("\n⚠️ Token graduation verification failed"); + console.log("Expected: trading=false, tradingOnUniswap=true"); + } + + // ============================================ + // Step 11: Test Sell (Post-Graduation if applicable) + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" Step 11: Test Sell"); + console.log("=".repeat(80)); + + // Note: After graduation, sell through BondingV2 should fail (trading=false) + // We'll test this behavior + const agentTokenBalanceForSell = await agentToken.balanceOf(signerAddress); + + if (BigInt(agentTokenBalanceForSell) === 0n) { + console.log("No tokens to sell (all used in graduation buys)"); + } else { + const sellAmount = BigInt(agentTokenBalanceForSell) / 4n; + console.log("Sell Amount:", formatEther(sellAmount), "tokens"); + + // Approve agent token to FRouterV2 + const agentTokenAllowance = await agentToken.allowance(signerAddress, config.fRouterV2Address); + if (agentTokenAllowance < sellAmount) { + console.log("\n--- Approving Agent Token to FRouterV2 ---"); + const approveAgentTx = await agentToken.approve(config.fRouterV2Address, ethers.MaxUint256); + await approveAgentTx.wait(); + console.log("✅ Approved Agent Token to FRouterV2"); + } + + if (graduated) { + // After graduation, sell through BondingV2 should fail + console.log("\n--- Testing sell after graduation (should fail) ---"); + try { + const sellDeadline = Math.floor(Date.now() / 1000) + 300; + await bondingV2.sell.staticCall(sellAmount, tokenAddress, 0, sellDeadline); + console.log("⚠️ Sell staticCall succeeded - unexpected after graduation"); + } catch (e: any) { + console.log("✅ Sell correctly rejected after graduation (trading=false)"); + console.log(" Error:", e.message?.substring(0, 100) || "InvalidTokenStatus"); + } + } else { + // Token not graduated, normal sell should work + const virtualBalanceBeforeSell = await virtualToken.balanceOf(signerAddress); + const taxVaultBalanceBeforeSell = await virtualToken.balanceOf(taxVault); + + console.log("\n--- Balances Before Sell ---"); + console.log("VIRTUAL Balance:", formatEther(virtualBalanceBeforeSell), "VIRTUAL"); + console.log("Tax Vault Balance:", formatEther(taxVaultBalanceBeforeSell), "VIRTUAL"); + + console.log("\n--- Executing sell ---"); + const sellDeadline = Math.floor(Date.now() / 1000) + 300; + const sellTx = await bondingV2.sell(sellAmount, tokenAddress, 0, sellDeadline, { gasLimit: 500000 }); + const sellReceipt = await sellTx.wait(); + console.log("✅ sell() transaction successful!"); + console.log("Gas Used:", sellReceipt.gasUsed.toString()); + + const virtualBalanceAfterSell = await virtualToken.balanceOf(signerAddress); + const taxVaultBalanceAfterSell = await virtualToken.balanceOf(taxVault); + + const virtualReceived = virtualBalanceAfterSell - virtualBalanceBeforeSell; + const sellTaxCollected = taxVaultBalanceAfterSell - taxVaultBalanceBeforeSell; + + console.log("\n--- Sell Results ---"); + console.log("VIRTUAL Received:", formatEther(virtualReceived), "VIRTUAL"); + console.log("Sell Tax Collected:", formatEther(sellTaxCollected), "VIRTUAL"); + console.log("Sell Tax Rate:", sellTax.toString(), "%"); + + if (BigInt(sellTaxCollected) > 0n) { + console.log("✅ Sell tax correctly collected to taxVault"); + } + } + } + + // ============================================ + // Summary + // ============================================ + console.log("\n" + "=".repeat(80)); + console.log(" E2E Test Summary"); + console.log("=".repeat(80)); + + console.log("\n✅ All BondingV2 tests completed!"); + console.log("\nVerified:"); + console.log(" 1. BondingV2 parameters are correctly configured (no BondingConfig)"); + console.log(" 2. FFactoryV2 tax parameters are correctly configured"); + console.log(" 3. preLaunch() executed successfully (required scheduled start time)"); + console.log(" 4. On-chain tokenInfo stored correctly"); + console.log(" 5. Fee collection correct:", formatEther(fee), "VIRTUAL"); + console.log(" 6. launch() executed successfully after start time"); + console.log(" 7. buy() during anti-sniper period - tax to antiSniperTaxVault"); + console.log(" 8. buy() after anti-sniper period - normal tax to taxVault"); + console.log(" 9. Buy to graduation - " + (graduated ? "✅ SUCCESS" : "⚠️ NOT TRIGGERED")); + console.log(" 10. Graduation state verification - " + (graduated ? "✅ VERIFIED" : "⚠️ SKIPPED")); + console.log(" 11. sell() - " + (graduated ? "correctly rejected after graduation" : "executed with correct tax")); + + console.log("\n--- Tax Summary ---"); + console.log("Normal Buy Tax Rate:", buyTax.toString(), "%"); + console.log("Normal Sell Tax Rate:", sellTax.toString(), "%"); + console.log("Anti-Sniper Buy Tax Start Value:", antiSniperBuyTaxStartValue.toString(), "%"); + console.log("Anti-Sniper Duration:", antiSniperDurationSeconds, "seconds (", antiSniperBuyTaxStartValue.toString(), " minutes)"); + console.log("Tax Vault:", taxVault); + console.log("Anti-Sniper Tax Vault:", antiSniperTaxVault); + + console.log("\n--- Token Summary ---"); + console.log("Token Address:", tokenAddress); + console.log("Pair Address:", pairAddress); + console.log("Virtual ID:", virtualId.toString()); + if (graduated) { + console.log("Agent Token:", tokenInfoAfterGrad.agentToken); + console.log("Graduation Status: ✅ GRADUATED"); + } else { + console.log("Graduation Status: ⚠️ NOT GRADUATED"); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error("\n❌ E2E Test Failed:"); + console.error(error); + process.exit(1); + }); diff --git a/scripts/launchpadv5/deployLaunchpadv5_3.ts b/scripts/launchpadv5/deployLaunchpadv5_3.ts index 0fc9cb8..2e7a9c3 100644 --- a/scripts/launchpadv5/deployLaunchpadv5_3.ts +++ b/scripts/launchpadv5/deployLaunchpadv5_3.ts @@ -91,9 +91,19 @@ const { ethers, upgrades } = require("hardhat"); throw new Error("AGENT_TAX_CONTRACT_ADDRESS not set in environment"); } - const maxAirdropPercent = process.env.MAX_AIRDROP_PERCENT; - if (!maxAirdropPercent) { - throw new Error("MAX_AIRDROP_PERCENT not set in environment"); + const maxAirdropBips = process.env.MAX_AIRDROP_BIPS; + if (!maxAirdropBips) { + throw new Error("MAX_AIRDROP_BIPS not set in environment"); + } + + const maxTotalReservedBips = process.env.MAX_TOTAL_RESERVED_BIPS; + if (!maxTotalReservedBips) { + throw new Error("MAX_TOTAL_RESERVED_BIPS not set in environment"); + } + + const acfReservedBips = process.env.ACF_RESERVED_BIPS; + if (!acfReservedBips) { + throw new Error("ACF_RESERVED_BIPS not set in environment"); } console.log("\nDeployment arguments loaded:", { @@ -113,7 +123,9 @@ const { ethers, upgrades } = require("hardhat"); teamTokenReservedWallet, fakeInitialVirtualLiq, targetRealVirtual, - maxAirdropPercent, + maxAirdropBips, + maxTotalReservedBips, + acfReservedBips, }); // ============================================ @@ -122,6 +134,12 @@ const { ethers, upgrades } = require("hardhat"); console.log("\n--- Deploying BondingConfig ---"); const BondingConfig = await ethers.getContractFactory("BondingConfig"); + const reserveSupplyParams = { + maxAirdropBips: maxAirdropBips, + maxTotalReservedBips: maxTotalReservedBips, + acfReservedBips: acfReservedBips, + }; + const scheduledLaunchParams = { startTimeDelay: startTimeDelay, normalLaunchFee: parseEther(normalLaunchFee).toString(), @@ -146,7 +164,7 @@ const { ethers, upgrades } = require("hardhat"); initialSupply, creationFeeToAddress, teamTokenReservedWallet, - maxAirdropPercent, + reserveSupplyParams, scheduledLaunchParams, deployParams, bondingCurveParams, @@ -231,11 +249,22 @@ const { ethers, upgrades } = require("hardhat"); // Deployer still has admin roles from previous scripts (_0, _1, _2) // Roles will be revoked in _4.ts console.log("\n--- Granting roles and configuring contracts ---"); - - const fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address); - const fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address); - const agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address); - const agentTax = await ethers.getContractAt("AgentTax", agentTaxAddress); + + var fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address); + var fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address); + var agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address); + var agentTax = await ethers.getContractAt("AgentTax", agentTaxAddress); + const adminPrivateKey = process.env.ADMIN_PRIVATE_KEY; + if (adminPrivateKey) { // If ADMIN_PRIVATE_KEY is set, use it to grant roles, easier for testnet + const adminSigner = new ethers.Wallet( + adminPrivateKey, + ethers.provider + ); + fFactoryV2 = await ethers.getContractAt("FFactoryV2", fFactoryV2Address, adminSigner); + fRouterV2 = await ethers.getContractAt("FRouterV2", fRouterV2Address, adminSigner); + agentFactoryV6 = await ethers.getContractAt("AgentFactoryV6", agentFactoryV6Address, adminSigner); + agentTax = await ethers.getContractAt("AgentTax", agentTaxAddress, adminSigner); + } // Grant CREATOR_ROLE of FFactoryV2 to BondingV5 const creatorRole = await fFactoryV2.CREATOR_ROLE(); @@ -278,7 +307,10 @@ const { ethers, upgrades } = require("hardhat"); console.log("\nConfiguration:"); console.log("- Initial Supply:", initialSupply); - console.log("- Max Airdrop Percent:", maxAirdropPercent, "%"); + console.log("- Reserve Supply Params (in bips, 1 bip = 0.01%):"); + console.log(" - Max Airdrop Bips:", maxAirdropBips, "(", Number(maxAirdropBips) / 100, "%)"); + console.log(" - Max Total Reserved Bips:", maxTotalReservedBips, "(", Number(maxTotalReservedBips) / 100, "%)"); + console.log(" - ACF Reserved Bips:", acfReservedBips, "(", Number(acfReservedBips) / 100, "%)"); console.log("- Start Time Delay:", startTimeDelay, "seconds"); console.log("- Normal Launch Fee:", normalLaunchFee, "VIRTUAL (scheduled/marketing)"); console.log("- ACF Fee:", acfFee, "VIRTUAL (extra fee when needAcf = true)"); diff --git a/scripts/launchpadv5/e2e_test.ts b/scripts/launchpadv5/e2e_test.ts index c058866..30a9ed8 100644 --- a/scripts/launchpadv5/e2e_test.ts +++ b/scripts/launchpadv5/e2e_test.ts @@ -100,16 +100,19 @@ async function main() { // Check BondingConfig parameters const scheduledLaunchParams = await bondingConfig.scheduledLaunchParams(); const bondingCurveParams = await bondingConfig.bondingCurveParams(); + const reserveSupplyParams = await bondingConfig.reserveSupplyParams(); const initialSupply = await bondingConfig.initialSupply(); - const maxAirdropPercent = await bondingConfig.maxAirdropPercent(); const feeTo = await bondingConfig.feeTo(); const teamTokenReservedWallet = await bondingConfig.teamTokenReservedWallet(); console.log("\n--- BondingConfig Parameters ---"); console.log("Initial Supply:", initialSupply.toString()); - console.log("Max Airdrop Percent:", maxAirdropPercent.toString(), "%"); console.log("Fee To:", feeTo); console.log("Team Token Reserved Wallet:", teamTokenReservedWallet); + console.log("\n--- ReserveSupplyParams (in bips, 1 bip = 0.01%) ---"); + console.log("Max Airdrop Bips:", reserveSupplyParams.maxAirdropBips.toString(), "(", Number(reserveSupplyParams.maxAirdropBips) / 100, "%)"); + console.log("Max Total Reserved Bips:", reserveSupplyParams.maxTotalReservedBips.toString(), "(", Number(reserveSupplyParams.maxTotalReservedBips) / 100, "%)"); + console.log("ACF Reserved Bips:", reserveSupplyParams.acfReservedBips.toString(), "(", Number(reserveSupplyParams.acfReservedBips) / 100, "%)"); console.log("\n--- ScheduledLaunchParams ---"); console.log("Start Time Delay:", scheduledLaunchParams.startTimeDelay.toString(), "seconds"); console.log("Normal Launch Fee:", formatEther(scheduledLaunchParams.normalLaunchFee), "VIRTUAL"); @@ -229,7 +232,7 @@ async function main() { const startTime = currentTimestamp + 100; // immediate launch (100 seconds from now) const launchMode = LAUNCH_MODE_NORMAL; - const airdropPercent = 3; // 3% airdrop + const airdropBips = 300; // 300 = 3.00% (in bips, 1 bip = 0.01%) const needAcf = true; // Test with ACF fee const antiSniperTaxType = ANTI_SNIPER_60S; // 60 seconds anti-sniper const isProject60days = false; @@ -247,7 +250,7 @@ async function main() { console.log("Scheduled Launch Start Time Delay:", startTimeDelayNum, "seconds"); console.log("Is Scheduled Launch:", isScheduledLaunch, "(expected: false - immediate launch)"); console.log("Launch Mode:", launchMode, "(NORMAL)"); - console.log("Airdrop Percent:", airdropPercent, "%"); + console.log("Airdrop Bips:", airdropBips, "(", airdropBips / 100, "%)"); console.log("Need ACF:", needAcf); console.log("Anti-Sniper Tax Type:", antiSniperTaxType, "(60S)"); console.log("Is Project 60 Days:", isProject60days); @@ -283,7 +286,7 @@ async function main() { purchaseAmount, startTime, launchMode, - airdropPercent, + airdropBips, needAcf, antiSniperTaxType, isProject60days @@ -322,7 +325,7 @@ async function main() { purchaseAmount, startTime, launchMode, - airdropPercent, + airdropBips, needAcf, antiSniperTaxType, isProject60days @@ -343,7 +346,7 @@ async function main() { purchaseAmount, startTime, launchMode, - airdropPercent, + airdropBips, needAcf, antiSniperTaxType, isProject60days, @@ -382,7 +385,7 @@ async function main() { console.log("Initial Purchase:", formatEther(initialPurchase), "VIRTUAL"); console.log("LaunchParams from Event:", { launchMode: eventLaunchParams.launchMode, - airdropPercent: eventLaunchParams.airdropPercent, + airdropBips: eventLaunchParams.airdropBips, needAcf: eventLaunchParams.needAcf, antiSniperTaxType: eventLaunchParams.antiSniperTaxType, isProject60days: eventLaunchParams.isProject60days, @@ -411,7 +414,7 @@ async function main() { const onChainLaunchParams = await bondingV5.tokenLaunchParams(tokenAddress); console.log("\n--- tokenLaunchParams (On-Chain) ---"); console.log("Launch Mode:", onChainLaunchParams.launchMode, "(expected:", launchMode, ")"); - console.log("Airdrop Percent:", onChainLaunchParams.airdropPercent, "(expected:", airdropPercent, ")"); + console.log("Airdrop Percent:", Number(onChainLaunchParams.airdropBips) / 100, "% (expected:", airdropBips / 100, "%)"); console.log("Need ACF:", onChainLaunchParams.needAcf, "(expected:", needAcf, ")"); console.log("Anti-Sniper Tax Type:", onChainLaunchParams.antiSniperTaxType, "(expected:", antiSniperTaxType, ")"); console.log("Is Project 60 Days:", onChainLaunchParams.isProject60days, "(expected:", isProject60days, ")"); diff --git a/test/launchpadv5/bondingV5.js b/test/launchpadv5/bondingV5.js index a2c9664..ba2346e 100644 --- a/test/launchpadv5/bondingV5.js +++ b/test/launchpadv5/bondingV5.js @@ -38,8 +38,10 @@ const LAUNCH_MODE_NORMAL = 0; const LAUNCH_MODE_X_LAUNCH = 1; const LAUNCH_MODE_ACP_SKILL = 2; -// Max airdrop percent -const MAX_AIRDROP_PERCENT = 5; +// Reserve supply parameters (in bips, 1 bip = 0.01%, e.g., 500 = 5.00%) +const MAX_AIRDROP_BIPS = 500; +const MAX_TOTAL_RESERVED_BIPS = 5500; // At least 45% must remain in bonding curve +const ACF_RESERVED_BIPS = 5000; // ACF operations reserve 50% // Anti-sniper tax type constants const ANTI_SNIPER_NONE = 0; @@ -252,6 +254,12 @@ async function setupBondingV5Test() { console.log("\n--- Deploying BondingConfig ---"); const BondingConfig = await ethers.getContractFactory("BondingConfig"); + const reserveSupplyParams = { + maxAirdropBips: MAX_AIRDROP_BIPS, + maxTotalReservedBips: MAX_TOTAL_RESERVED_BIPS, + acfReservedBips: ACF_RESERVED_BIPS, + }; + const scheduledLaunchParams = { startTimeDelay: START_TIME_DELAY, normalLaunchFee: NORMAL_LAUNCH_FEE, @@ -276,7 +284,7 @@ async function setupBondingV5Test() { INITIAL_SUPPLY, owner.address, // feeTo beOpsWallet.address, // teamTokenReservedWallet - MAX_AIRDROP_PERCENT, // maxAirdropPercent + reserveSupplyParams, // reserveSupplyParams scheduledLaunchParams, deployParams, bondingCurveParams, @@ -495,7 +503,7 @@ describe("BondingV5", function () { purchaseAmount, startTime, LAUNCH_MODE_NORMAL, // launchMode_ - 0, // airdropPercent_ + 0, // airdropBips_ false, // needAcf_ ANTI_SNIPER_60S, // antiSniperTaxType_ false // isProject60days_ @@ -528,7 +536,7 @@ describe("BondingV5", function () { // Verify tokenLaunchParams is stored correctly const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); - expect(launchParams.airdropPercent).to.equal(0); + expect(launchParams.airdropBips).to.equal(0); expect(launchParams.needAcf).to.be.false; expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_60S); expect(launchParams.isProject60days).to.be.false; @@ -895,7 +903,7 @@ describe("BondingV5", function () { purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, - 0, // airdropPercent must be 0 for special modes + 0, // airdropBips must be 0 for special modes false, // needAcf must be false for special modes ANTI_SNIPER_NONE, // antiSniperTaxType must be NONE for special modes false // isProject60days must be false for special modes @@ -966,7 +974,7 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + 100; - // Should revert with non-zero airdropPercent (special modes require 0) + // Should revert with non-zero airdropBips (special modes require 0) await expect( bondingV5.connect(user1).preLaunch( "Invalid X Launch", @@ -978,7 +986,7 @@ describe("BondingV5", function () { purchaseAmount, startTime, LAUNCH_MODE_X_LAUNCH, - 5, // airdropPercent = 5 (within maxAirdropPercent but special modes require 0) + 500, // airdropBips = 500 (5.00%, within maxAirdropBips but special modes require 0) false, ANTI_SNIPER_NONE, false @@ -1237,7 +1245,7 @@ describe("BondingV5", function () { // ============================================ describe("Configurable Options Permutations", function () { - describe("airdropPercent Variations", function () { + describe("airdropBips Variations", function () { it("Should create token with 0% airdrop", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1261,7 +1269,7 @@ describe("BondingV5", function () { const tokenAddress = bondingV5.interface.parseLog(event).args.token; const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); - expect(launchParams.airdropPercent).to.equal(0); + expect(launchParams.airdropBips).to.equal(0); }); it("Should create token with max airdrop (5%)", async function () { @@ -1276,7 +1284,7 @@ describe("BondingV5", function () { "Max Airdrop Token", "T5", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false // MAX_AIRDROP_PERCENT = 5 + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_60S, false // MAX_AIRDROP_BIPS = 500 (5.00%) ); const receipt = await tx.wait(); @@ -1287,7 +1295,7 @@ describe("BondingV5", function () { const tokenAddress = bondingV5.interface.parseLog(event).args.token; const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); - expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.airdropBips).to.equal(500); }); it("Should create token with 3% airdrop", async function () { @@ -1302,7 +1310,7 @@ describe("BondingV5", function () { "3% Airdrop Token", "T3", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 3, false, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 300, false, ANTI_SNIPER_60S, false // 300 = 3.00% ); const receipt = await tx.wait(); @@ -1313,10 +1321,10 @@ describe("BondingV5", function () { const tokenAddress = bondingV5.interface.parseLog(event).args.token; const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); - expect(launchParams.airdropPercent).to.equal(3); + expect(launchParams.airdropBips).to.equal(300); }); - it("Should revert with airdropPercent exceeding MAX_AIRDROP_PERCENT (6% > 5%)", async function () { + it("Should revert with airdropBips exceeding MAX_AIRDROP_BIPS (6% > 5%)", async function () { const { user1 } = accounts; const { bondingV5, virtualToken, bondingConfig } = contracts; @@ -1330,9 +1338,9 @@ describe("BondingV5", function () { "Exceed Airdrop", "EXC", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 6, false, ANTI_SNIPER_60S, false // 6% > MAX_AIRDROP_PERCENT (5%) + LAUNCH_MODE_NORMAL, 600, false, ANTI_SNIPER_60S, false // 600 (6.00%) > MAX_AIRDROP_BIPS (500 = 5.00%) ) - ).to.be.revertedWithCustomError(bondingConfig, "AirdropPercentExceedsMax"); + ).to.be.revertedWithCustomError(bondingConfig, "AirdropBipsExceedsMax"); }); }); @@ -1403,7 +1411,7 @@ describe("BondingV5", function () { expect(feeToBalanceAfter).to.equal(feeToBalanceBefore); }); - it("Should revert if needAcf = true and airdropPercent causes total to exceed limit", async function () { + it("Should revert if needAcf = true and airdropBips causes total to exceed limit", async function () { const { user1 } = accounts; const { bondingV5, virtualToken, bondingConfig } = contracts; @@ -1412,18 +1420,18 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // needAcf = true adds 50% reserve, so total = 5 + 50 = 55 >= MAX_TOTAL_RESERVED_PERCENT (55) + // needAcf = true adds 5000 (50%) reserve, so total = 500 + 5000 = 5500 >= MAX_TOTAL_RESERVED_BIPS (5500) await expect( bondingV5.connect(user1).preLaunch( "ACF Exceed", "ACFE", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, true, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 (5.00%) + 5000 (50%) = 5500 (55%) ) - ).to.be.revertedWithCustomError(bondingConfig, "InvalidReservePercent"); + ).to.be.revertedWithCustomError(bondingConfig, "InvalidReserveBips"); }); - it("Should allow needAcf = true with airdropPercent = 5 (total 55%)", async function () { + it("Should allow needAcf = true with airdropBips = 400 (total 54%)", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1432,12 +1440,12 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // needAcf = true adds 50% reserve, so total = 4 + 50 = 54 < 55 OK + // needAcf = true adds 5000 (50%) reserve, so total = 400 + 5000 = 5400 < 5500 OK const tx = await bondingV5.connect(user1).preLaunch( "ACF With Airdrop", "ACFA", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 400, true, ANTI_SNIPER_60S, false // 400 (4.00%) + 5000 (50%) = 5400 (54%) ); const receipt = await tx.wait(); @@ -1645,7 +1653,7 @@ describe("BondingV5", function () { await bondingConfig.connect(owner).setAcpSkillLauncher(user1.address, true); }); - it("Should revert X_LAUNCH with non-zero airdropPercent", async function () { + it("Should revert X_LAUNCH with non-zero airdropBips", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1741,7 +1749,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "InvalidSpecialLaunchParams"); }); - it("Should revert ACP_SKILL with non-zero airdropPercent", async function () { + it("Should revert ACP_SKILL with non-zero airdropBips", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1854,7 +1862,7 @@ describe("BondingV5", function () { "Event Test Token", "EVT", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_98M, true // Use MAX_AIRDROP_PERCENT (5) + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_98M, true // 500 = 5.00% ); const receipt = await tx.wait(); @@ -1868,7 +1876,7 @@ describe("BondingV5", function () { // Verify LaunchParams in event const launchParams = parsedEvent.args.launchParams; expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); - expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.airdropBips).to.equal(500); expect(launchParams.needAcf).to.equal(false); expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); expect(launchParams.isProject60days).to.equal(true); @@ -1886,7 +1894,7 @@ describe("BondingV5", function () { "Launch Event Token", "LET", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_60S, false // 500 = 5.00% ); let receipt = await tx.wait(); @@ -1911,7 +1919,7 @@ describe("BondingV5", function () { // Verify LaunchParams in Launched event const launchParams = parsedEvent.args.launchParams; expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); - expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.airdropBips).to.equal(500); expect(launchParams.needAcf).to.equal(false); expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_60S); expect(launchParams.isProject60days).to.equal(false); @@ -1922,7 +1930,7 @@ describe("BondingV5", function () { // Token Graduation Threshold Tests // ============================================ describe("Token Graduation Threshold Calculation", function () { - it("Should calculate different gradThreshold for different airdropPercent", async function () { + it("Should calculate different gradThreshold for different airdropBips", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -1945,14 +1953,14 @@ describe("BondingV5", function () { const token1 = bondingV5.interface.parseLog(event).args.token; const gradThreshold1 = await bondingV5.tokenGradThreshold(token1); - // Token 2: 5% airdrop (MAX_AIRDROP_PERCENT) + // Token 2: 5% airdrop (MAX_AIRDROP_BIPS = 500) await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); startTime = (await time.latest()) + START_TIME_DELAY + 1; tx = await bondingV5.connect(user1).preLaunch( "5% Airdrop Grad", "G5", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_60S, false // 500 = 5.00% ); receipt = await tx.wait(); event = receipt.logs.find((log) => { @@ -2019,7 +2027,7 @@ describe("BondingV5", function () { ).to.be.revertedWithCustomError(bondingV5, "LaunchModeNotEnabled"); }); - it("Should allow exact boundary of MAX_TOTAL_RESERVED_PERCENT (needAcf + 4% = 54%)", async function () { + it("Should allow exact boundary of MAX_TOTAL_RESERVED_BIPS (needAcf + 4% = 54%)", async function () { const { user1 } = accounts; const { bondingV5, virtualToken } = contracts; @@ -2028,19 +2036,19 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // needAcf (50%) + 4% = 54% should work (just under 55% limit) + // needAcf (5000 = 50%) + 400 (4%) = 5400 (54%) should work (just under 5500 = 55% limit) const tx = await bondingV5.connect(user1).preLaunch( "Boundary Test", "BNDY", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 400, true, ANTI_SNIPER_60S, false // 400 = 4.00% ); const receipt = await tx.wait(); expect(receipt).to.not.be.undefined; }); - it("Should revert at exact MAX_TOTAL_RESERVED_PERCENT (needAcf + 5% = 55%)", async function () { + it("Should revert at exact MAX_TOTAL_RESERVED_BIPS (needAcf + 5% = 55%)", async function () { const { user1 } = accounts; const { bondingV5, virtualToken, bondingConfig } = contracts; @@ -2049,15 +2057,15 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // needAcf (50%) + 5% = 55% should fail (at limit) + // needAcf (5000 = 50%) + 500 (5%) = 5500 (55%) should fail (at limit) await expect( bondingV5.connect(user1).preLaunch( "Over Limit", "OVER", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, true, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 = 5.00% ) - ).to.be.revertedWithCustomError(bondingConfig, "InvalidReservePercent"); + ).to.be.revertedWithCustomError(bondingConfig, "InvalidReserveBips"); }); it("Should allow needAcf = true with 0% airdrop (total 50%)", async function () { @@ -2085,7 +2093,7 @@ describe("BondingV5", function () { const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); expect(launchParams.needAcf).to.be.true; - expect(launchParams.airdropPercent).to.equal(0); + expect(launchParams.airdropBips).to.equal(0); }); it("Should allow exactly 4% airdrop + ACF (total 54%)", async function () { @@ -2097,12 +2105,12 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // 4% + 50% (ACF) = 54% < 55% limit + // 400 (4%) + 5000 (50% ACF) = 5400 (54%) < 5500 (55%) limit const tx = await bondingV5.connect(user1).preLaunch( "Max ACF Combo", "MXAC", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 400, true, ANTI_SNIPER_60S, false // 400 = 4.00% ); const receipt = await tx.wait(); @@ -2118,15 +2126,15 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // 5% + 50% (ACF) = 55% >= 55% limit + // 500 (5%) + 5000 (50% ACF) = 5500 (55%) >= 5500 (55%) limit await expect( bondingV5.connect(user1).preLaunch( "Over ACF Combo", "OVAC", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, true, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 = 5.00% ) - ).to.be.revertedWithCustomError(bondingConfig, "InvalidReservePercent"); + ).to.be.revertedWithCustomError(bondingConfig, "InvalidReserveBips"); }); }); @@ -2158,7 +2166,7 @@ describe("BondingV5", function () { const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); - expect(launchParams.airdropPercent).to.equal(0); + expect(launchParams.airdropBips).to.equal(0); expect(launchParams.needAcf).to.equal(false); expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_NONE); expect(launchParams.isProject60days).to.equal(false); @@ -2176,7 +2184,7 @@ describe("BondingV5", function () { "Max Params", "MAXP", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_98M, true // MAX_AIRDROP_PERCENT = 5 + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_98M, true // MAX_AIRDROP_BIPS = 500 (5.00%) ); const receipt = await tx.wait(); @@ -2187,7 +2195,7 @@ describe("BondingV5", function () { const tokenAddress = bondingV5.interface.parseLog(event).args.token; const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); - expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.airdropBips).to.equal(500); expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); expect(launchParams.isProject60days).to.equal(true); }); @@ -2199,13 +2207,13 @@ describe("BondingV5", function () { const purchaseAmount = ethers.parseEther("1000"); const tokens = []; - // Test matrix of parameter combinations (airdrop values <= MAX_AIRDROP_PERCENT = 5) + // Test matrix of parameter combinations (airdrop values <= MAX_AIRDROP_BIPS = 500) const testCases = [ { airdrop: 0, needAcf: false, antiSniper: ANTI_SNIPER_NONE, is60days: false }, - { airdrop: 3, needAcf: false, antiSniper: ANTI_SNIPER_60S, is60days: true }, - { airdrop: 5, needAcf: false, antiSniper: ANTI_SNIPER_98M, is60days: false }, + { airdrop: 300, needAcf: false, antiSniper: ANTI_SNIPER_60S, is60days: true }, // 300 = 3.00% + { airdrop: 500, needAcf: false, antiSniper: ANTI_SNIPER_98M, is60days: false }, // 500 = 5.00% { airdrop: 0, needAcf: true, antiSniper: ANTI_SNIPER_60S, is60days: false }, - { airdrop: 4, needAcf: true, antiSniper: ANTI_SNIPER_98M, is60days: true }, + { airdrop: 400, needAcf: true, antiSniper: ANTI_SNIPER_98M, is60days: true }, // 400 = 4.00% ]; for (let i = 0; i < testCases.length; i++) { @@ -2230,7 +2238,7 @@ describe("BondingV5", function () { // Verify stored parameters const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); - expect(launchParams.airdropPercent).to.equal(tc.airdrop); + expect(launchParams.airdropBips).to.equal(tc.airdrop); expect(launchParams.needAcf).to.equal(tc.needAcf); expect(launchParams.antiSniperTaxType).to.equal(tc.antiSniper); expect(launchParams.isProject60days).to.equal(tc.is60days); @@ -2257,7 +2265,7 @@ describe("BondingV5", function () { "Regression Token", "REGT", [0, 1, 2], "A regression test token", "https://example.com/image.png", ["url1", "url2", "url3", "url4"], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, true // Use MAX_AIRDROP_PERCENT (5) + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_60S, true // 500 = 5.00% ); const receipt = await tx.wait(); @@ -2325,7 +2333,7 @@ describe("BondingV5", function () { "Preserve Token", "PRSV", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_98M, true // Use MAX_AIRDROP_PERCENT (5) + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_98M, true // 500 = 5.00% ); const receipt = await tx.wait(); @@ -2341,7 +2349,7 @@ describe("BondingV5", function () { // Verify launch params are still correct after launch const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); - expect(launchParams.airdropPercent).to.equal(5); + expect(launchParams.airdropBips).to.equal(500); expect(launchParams.needAcf).to.equal(false); expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); expect(launchParams.isProject60days).to.equal(true); @@ -2488,7 +2496,7 @@ describe("BondingV5", function () { INITIAL_SUPPLY, owner.address, owner.address, - MAX_AIRDROP_PERCENT, + { maxAirdropBips: MAX_AIRDROP_BIPS, maxTotalReservedBips: MAX_TOTAL_RESERVED_BIPS, acfReservedBips: ACF_RESERVED_BIPS }, { startTimeDelay: START_TIME_DELAY, normalLaunchFee: NORMAL_LAUNCH_FEE, acfFee: ACF_FEE }, { tbaSalt: TBA_SALT, tbaImplementation: TBA_IMPLEMENTATION, daoVotingPeriod: DAO_VOTING_PERIOD, daoThreshold: DAO_THRESHOLD }, { fakeInitialVirtualLiq: FAKE_INITIAL_VIRTUAL_LIQ, targetRealVirtual: TARGET_REAL_VIRTUAL }, @@ -2543,7 +2551,7 @@ describe("BondingV5", function () { purchaseAmount, startTime, LAUNCH_MODE_NORMAL, - 3, + 300, // 300 = 3.00% true, ANTI_SNIPER_98M, true @@ -2619,7 +2627,7 @@ describe("BondingV5", function () { const launchParams = await bondingV5.tokenLaunchParams(tokenAddress); expect(launchParams.launchMode).to.equal(LAUNCH_MODE_NORMAL); - expect(launchParams.airdropPercent).to.equal(3); + expect(launchParams.airdropBips).to.equal(300); // 300 = 3.00% expect(launchParams.needAcf).to.be.true; expect(launchParams.antiSniperTaxType).to.equal(ANTI_SNIPER_98M); expect(launchParams.isProject60days).to.be.true; @@ -2639,17 +2647,17 @@ describe("BondingV5", function () { const supply100 = await bondingConfig.calculateBondingCurveSupply(0, false); expect(supply100).to.equal(initialSupply); - // 5% airdrop, no ACF: 95% bonding curve - const supply95 = await bondingConfig.calculateBondingCurveSupply(5, false); - expect(supply95).to.equal((initialSupply * 95n) / 100n); + // 5% airdrop (500), no ACF: 95% bonding curve (9500/10000) + const supply95 = await bondingConfig.calculateBondingCurveSupply(500, false); + expect(supply95).to.equal((initialSupply * 9500n) / 10000n); - // 0% airdrop, with ACF: 50% bonding curve + // 0% airdrop, with ACF (5000 = 50%): 50% bonding curve (5000/10000) const supply50 = await bondingConfig.calculateBondingCurveSupply(0, true); - expect(supply50).to.equal((initialSupply * 50n) / 100n); + expect(supply50).to.equal((initialSupply * 5000n) / 10000n); - // 4% airdrop, with ACF: 46% bonding curve - const supply46 = await bondingConfig.calculateBondingCurveSupply(4, true); - expect(supply46).to.equal((initialSupply * 46n) / 100n); + // 4% airdrop (400), with ACF (5000): 46% bonding curve (4600/10000) + const supply46 = await bondingConfig.calculateBondingCurveSupply(400, true); + expect(supply46).to.equal((initialSupply * 4600n) / 10000n); }); it("Should correctly identify special modes", async function () { @@ -2811,13 +2819,13 @@ describe("BondingV5", function () { const reservedWalletBalanceBefore = await ethers.provider.getBalance(beOpsWallet.address); - // Create token with 5% airdrop (should transfer 5% to reserved wallet) + // Create token with 5% airdrop (500 = 5.00%, should transfer 5% to reserved wallet) const startTime = (await time.latest()) + START_TIME_DELAY + 1; const tx = await bondingV5.connect(user1).preLaunch( "Reserved Test Token", "RTT", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 5, false, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 500, false, ANTI_SNIPER_60S, false // 500 = 5.00% ); const receipt = await tx.wait(); @@ -2843,13 +2851,13 @@ describe("BondingV5", function () { const purchaseAmount = ethers.parseEther("1000"); await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); - // Create token with 4% airdrop and needAcf = true (54% total reserved) + // Create token with 4% airdrop (400) and needAcf = true (5400 = 54% total reserved) const startTime = (await time.latest()) + START_TIME_DELAY + 1; const tx = await bondingV5.connect(user1).preLaunch( "ACF Reserved Test", "ART", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 4, true, ANTI_SNIPER_60S, false + LAUNCH_MODE_NORMAL, 400, true, ANTI_SNIPER_60S, false // 400 = 4.00% ); const receipt = await tx.wait(); From 854e4af3b5becbdd6913e42f126cd57d2d05f375 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Thu, 12 Mar 2026 20:16:18 +0800 Subject: [PATCH 6/8] make scheduledLaunchParams and deployParams var public --- .openzeppelin/base-sepolia.json | 720 ++++++++++++++++++++++++ contracts/launchpadv2/BondingConfig.sol | 36 +- contracts/launchpadv2/BondingV5.sol | 12 +- scripts/launchpadv5/e2e_test.ts | 2 +- test/launchpadv5/bondingV5.js | 4 +- 5 files changed, 749 insertions(+), 25 deletions(-) diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json index 9af8340..27f3930 100644 --- a/.openzeppelin/base-sepolia.json +++ b/.openzeppelin/base-sepolia.json @@ -47935,6 +47935,726 @@ ] } } + }, + "9f71928f9b53a257c2c4a26fa6220cda368dd2fab4e9b806cb8ac641dd65f652": { + "address": "0xe05D92004dd1A6a1FeceF6930bB47CC1D8fb02A1", + "txHash": "0xf2c36d6639e611fac6db394c20271e5ff0b1789b727c36d2d2e7652418c183f8", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "factory", + "offset": 0, + "slot": "0", + "type": "t_contract(IFFactoryV2Minimal)1992", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:68" + }, + { + "label": "router", + "offset": 0, + "slot": "1", + "type": "t_contract(IFRouterV2Minimal)2054", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:69" + }, + { + "label": "agentFactory", + "offset": 0, + "slot": "2", + "type": "t_contract(IAgentFactoryV6Minimal)2119", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:70" + }, + { + "label": "bondingConfig", + "offset": 0, + "slot": "3", + "type": "t_contract(BondingConfig)1959", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:71" + }, + { + "label": "tokenInfo", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_address,t_struct(Token)1435_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:73" + }, + { + "label": "tokenInfos", + "offset": 0, + "slot": "5", + "type": "t_array(t_address)dyn_storage", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:74" + }, + { + "label": "tokenLaunchParams", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_struct(LaunchParams)1446_storage)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:80" + }, + { + "label": "tokenGradThreshold", + "offset": 0, + "slot": "7", + "type": "t_mapping(t_address,t_uint256)", + "contract": "BondingV5", + "src": "contracts/launchpadv2/BondingV5.sol:83" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ReentrancyGuardStorage)158_storage": { + "label": "struct ReentrancyGuardUpgradeable.ReentrancyGuardStorage", + "members": [ + { + "label": "_status", + "type": "t_uint256", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_address)dyn_storage": { + "label": "address[]", + "numberOfBytes": "32" + }, + "t_array(t_uint8)dyn_storage": { + "label": "uint8[]", + "numberOfBytes": "32" + }, + "t_contract(BondingConfig)1959": { + "label": "contract BondingConfig", + "numberOfBytes": "20" + }, + "t_contract(IAgentFactoryV6Minimal)2119": { + "label": "contract IAgentFactoryV6Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFFactoryV2Minimal)1992": { + "label": "contract IFFactoryV2Minimal", + "numberOfBytes": "20" + }, + "t_contract(IFRouterV2Minimal)2054": { + "label": "contract IFRouterV2Minimal", + "numberOfBytes": "20" + }, + "t_mapping(t_address,t_struct(LaunchParams)1446_storage)": { + "label": "mapping(address => struct BondingConfig.LaunchParams)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_struct(Token)1435_storage)": { + "label": "mapping(address => struct BondingConfig.Token)", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_uint256)": { + "label": "mapping(address => uint256)", + "numberOfBytes": "32" + }, + "t_string_storage": { + "label": "string", + "numberOfBytes": "32" + }, + "t_struct(Data)1396_storage": { + "label": "struct BondingConfig.Data", + "members": [ + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "name", + "type": "t_string_storage", + "offset": 0, + "slot": "1" + }, + { + "label": "_name", + "type": "t_string_storage", + "offset": 0, + "slot": "2" + }, + { + "label": "ticker", + "type": "t_string_storage", + "offset": 0, + "slot": "3" + }, + { + "label": "supply", + "type": "t_uint256", + "offset": 0, + "slot": "4" + }, + { + "label": "price", + "type": "t_uint256", + "offset": 0, + "slot": "5" + }, + { + "label": "marketCap", + "type": "t_uint256", + "offset": 0, + "slot": "6" + }, + { + "label": "liquidity", + "type": "t_uint256", + "offset": 0, + "slot": "7" + }, + { + "label": "volume", + "type": "t_uint256", + "offset": 0, + "slot": "8" + }, + { + "label": "volume24H", + "type": "t_uint256", + "offset": 0, + "slot": "9" + }, + { + "label": "prevPrice", + "type": "t_uint256", + "offset": 0, + "slot": "10" + }, + { + "label": "lastUpdated", + "type": "t_uint256", + "offset": 0, + "slot": "11" + } + ], + "numberOfBytes": "384" + }, + "t_struct(LaunchParams)1446_storage": { + "label": "struct BondingConfig.LaunchParams", + "members": [ + { + "label": "launchMode", + "type": "t_uint8", + "offset": 0, + "slot": "0" + }, + { + "label": "airdropBips", + "type": "t_uint16", + "offset": 1, + "slot": "0" + }, + { + "label": "needAcf", + "type": "t_bool", + "offset": 3, + "slot": "0" + }, + { + "label": "antiSniperTaxType", + "type": "t_uint8", + "offset": 4, + "slot": "0" + }, + { + "label": "isProject60days", + "type": "t_bool", + "offset": 5, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(Token)1435_storage": { + "label": "struct BondingConfig.Token", + "members": [ + { + "label": "creator", + "type": "t_address", + "offset": 0, + "slot": "0" + }, + { + "label": "token", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "pair", + "type": "t_address", + "offset": 0, + "slot": "2" + }, + { + "label": "agentToken", + "type": "t_address", + "offset": 0, + "slot": "3" + }, + { + "label": "data", + "type": "t_struct(Data)1396_storage", + "offset": 0, + "slot": "4" + }, + { + "label": "description", + "type": "t_string_storage", + "offset": 0, + "slot": "16" + }, + { + "label": "cores", + "type": "t_array(t_uint8)dyn_storage", + "offset": 0, + "slot": "17" + }, + { + "label": "image", + "type": "t_string_storage", + "offset": 0, + "slot": "18" + }, + { + "label": "twitter", + "type": "t_string_storage", + "offset": 0, + "slot": "19" + }, + { + "label": "telegram", + "type": "t_string_storage", + "offset": 0, + "slot": "20" + }, + { + "label": "youtube", + "type": "t_string_storage", + "offset": 0, + "slot": "21" + }, + { + "label": "website", + "type": "t_string_storage", + "offset": 0, + "slot": "22" + }, + { + "label": "trading", + "type": "t_bool", + "offset": 0, + "slot": "23" + }, + { + "label": "tradingOnUniswap", + "type": "t_bool", + "offset": 1, + "slot": "23" + }, + { + "label": "applicationId", + "type": "t_uint256", + "offset": 0, + "slot": "24" + }, + { + "label": "initialPurchase", + "type": "t_uint256", + "offset": 0, + "slot": "25" + }, + { + "label": "virtualId", + "type": "t_uint256", + "offset": 0, + "slot": "26" + }, + { + "label": "launchExecuted", + "type": "t_bool", + "offset": 0, + "slot": "27" + } + ], + "numberOfBytes": "896" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint8": { + "label": "uint8", + "numberOfBytes": "1" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.ReentrancyGuard": [ + { + "contract": "ReentrancyGuardUpgradeable", + "label": "_status", + "type": "t_uint256", + "src": "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol:40", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + }, + "3623ee768d4f763cfe556a56fae46b8c2a3d97506f989aad5a001102e4238632": { + "address": "0x8DdccF04668456716c08284E660b00411d8B5926", + "txHash": "0xb65543830f7dcde4be1b709b0257d330a9ad4a915773fdf2e2ddd818652131de", + "layout": { + "solcVersion": "0.8.26", + "storage": [ + { + "label": "reserveSupplyParams", + "offset": 0, + "slot": "0", + "type": "t_struct(ReserveSupplyParams)1331_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:26" + }, + { + "label": "scheduledLaunchParams", + "offset": 0, + "slot": "1", + "type": "t_struct(ScheduledLaunchParams)1350_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:41" + }, + { + "label": "teamTokenReservedWallet", + "offset": 0, + "slot": "4", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:44" + }, + { + "label": "isXLauncher", + "offset": 0, + "slot": "5", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:47" + }, + { + "label": "isAcpSkillLauncher", + "offset": 0, + "slot": "6", + "type": "t_mapping(t_address,t_bool)", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:48" + }, + { + "label": "bondingCurveParams", + "offset": 0, + "slot": "7", + "type": "t_struct(BondingCurveParams)1368_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:55" + }, + { + "label": "deployParams", + "offset": 0, + "slot": "9", + "type": "t_struct(DeployParams)1455_storage", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:110" + }, + { + "label": "initialSupply", + "offset": 0, + "slot": "12", + "type": "t_uint256", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:113" + }, + { + "label": "feeTo", + "offset": 0, + "slot": "13", + "type": "t_address", + "contract": "BondingConfig", + "src": "contracts/launchpadv2/BondingConfig.sol:114" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_struct(InitializableStorage)73_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(OwnableStorage)13_storage": { + "label": "struct OwnableUpgradeable.OwnableStorage", + "members": [ + { + "label": "_owner", + "type": "t_address", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_struct(BondingCurveParams)1368_storage": { + "label": "struct BondingConfig.BondingCurveParams", + "members": [ + { + "label": "fakeInitialVirtualLiq", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "targetRealVirtual", + "type": "t_uint256", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_struct(DeployParams)1455_storage": { + "label": "struct BondingConfig.DeployParams", + "members": [ + { + "label": "tbaSalt", + "type": "t_bytes32", + "offset": 0, + "slot": "0" + }, + { + "label": "tbaImplementation", + "type": "t_address", + "offset": 0, + "slot": "1" + }, + { + "label": "daoVotingPeriod", + "type": "t_uint32", + "offset": 20, + "slot": "1" + }, + { + "label": "daoThreshold", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_struct(ReserveSupplyParams)1331_storage": { + "label": "struct BondingConfig.ReserveSupplyParams", + "members": [ + { + "label": "maxAirdropBips", + "type": "t_uint16", + "offset": 0, + "slot": "0" + }, + { + "label": "maxTotalReservedBips", + "type": "t_uint16", + "offset": 2, + "slot": "0" + }, + { + "label": "acfReservedBips", + "type": "t_uint16", + "offset": 4, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(ScheduledLaunchParams)1350_storage": { + "label": "struct BondingConfig.ScheduledLaunchParams", + "members": [ + { + "label": "startTimeDelay", + "type": "t_uint256", + "offset": 0, + "slot": "0" + }, + { + "label": "normalLaunchFee", + "type": "t_uint256", + "offset": 0, + "slot": "1" + }, + { + "label": "acfFee", + "type": "t_uint256", + "offset": 0, + "slot": "2" + } + ], + "numberOfBytes": "96" + }, + "t_uint16": { + "label": "uint16", + "numberOfBytes": "2" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + }, + "t_uint32": { + "label": "uint32", + "numberOfBytes": "4" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.Ownable": [ + { + "contract": "OwnableUpgradeable", + "label": "_owner", + "type": "t_address", + "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:24", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } } } } diff --git a/contracts/launchpadv2/BondingConfig.sol b/contracts/launchpadv2/BondingConfig.sol index 73d405c..25a18c6 100644 --- a/contracts/launchpadv2/BondingConfig.sol +++ b/contracts/launchpadv2/BondingConfig.sol @@ -37,9 +37,8 @@ contract BondingConfig is Initializable, OwnableUpgradeable { uint256 normalLaunchFee; // Fee for scheduled launches / marketing (e.g., 100 VIRTUAL) uint256 acfFee; // Extra fee when needAcf = true (e.g., 10 VIRTUAL on base, 150 VIRTUAL on eth) } - // Note: Using internal + view function because Solidity auto-getter for structs - // returns individual fields, not the whole struct as memory - ScheduledLaunchParams internal _scheduledLaunchParams; + // public for Etherscan visibility; use getScheduledLaunchParams() for memory struct + ScheduledLaunchParams public scheduledLaunchParams; // Global wallet to receive reserved tokens (airdrop + ACF) address public teamTokenReservedWallet; @@ -107,9 +106,8 @@ contract BondingConfig is Initializable, OwnableUpgradeable { uint32 daoVotingPeriod; uint256 daoThreshold; } - // Note: Using internal + view function because Solidity auto-getter for structs - // returns individual fields, not the whole struct as memory - DeployParams internal _deployParams; + // public for Etherscan visibility; use getDeployParams() for memory struct + DeployParams public deployParams; // Common parameters uint256 public initialSupply; @@ -145,8 +143,8 @@ contract BondingConfig is Initializable, OwnableUpgradeable { feeTo = feeTo_; teamTokenReservedWallet = teamTokenReservedWallet_; reserveSupplyParams = reserveSupplyParams_; - _scheduledLaunchParams = scheduledLaunchParams_; - _deployParams = deployParams_; + scheduledLaunchParams = scheduledLaunchParams_; + deployParams = deployParams_; bondingCurveParams = bondingCurveParams_; } @@ -155,7 +153,7 @@ contract BondingConfig is Initializable, OwnableUpgradeable { * @param params_ The deploy parameters */ function setDeployParams(DeployParams memory params_) external onlyOwner { - _deployParams = params_; + deployParams = params_; emit DeployParamsUpdated(params_); } @@ -242,7 +240,7 @@ contract BondingConfig is Initializable, OwnableUpgradeable { function setScheduledLaunchParams( ScheduledLaunchParams memory params_ ) external onlyOwner { - _scheduledLaunchParams = params_; + scheduledLaunchParams = params_; } /** @@ -332,16 +330,22 @@ contract BondingConfig is Initializable, OwnableUpgradeable { revert InvalidAntiSniperType(); } - function scheduledLaunchParams() + /** + * @notice Get scheduled launch params as memory struct (for contract calls) + */ + function getScheduledLaunchParams() external view returns (ScheduledLaunchParams memory) { - return _scheduledLaunchParams; + return scheduledLaunchParams; } - function deployParams() external view returns (DeployParams memory) { - return _deployParams; + /** + * @notice Get deploy params as memory struct (for contract calls) + */ + function getDeployParams() external view returns (DeployParams memory) { + return deployParams; } /** @@ -361,10 +365,10 @@ contract BondingConfig is Initializable, OwnableUpgradeable { ) external view returns (uint256) { uint256 totalFee = 0; if (isScheduledLaunch_) { - totalFee += _scheduledLaunchParams.normalLaunchFee; + totalFee += scheduledLaunchParams.normalLaunchFee; } if (needAcf_) { - totalFee += _scheduledLaunchParams.acfFee; + totalFee += scheduledLaunchParams.acfFee; } return totalFee; } diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 317a266..926ca89 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -164,7 +164,7 @@ contract BondingV5 is // Determine if this is an immediate launch or scheduled launch // Immediate launch: startTime < now + scheduledLaunchStartTimeDelay // Scheduled launch: startTime >= now + scheduledLaunchStartTimeDelay - BondingConfig.ScheduledLaunchParams memory scheduledParams = bondingConfig.scheduledLaunchParams(); + BondingConfig.ScheduledLaunchParams memory scheduledParams = bondingConfig.getScheduledLaunchParams(); uint256 scheduledThreshold = block.timestamp + scheduledParams.startTimeDelay; bool isScheduledLaunch = startTime_ >= scheduledThreshold; @@ -216,7 +216,7 @@ contract BondingV5 is ); uint256 configInitialSupply = bondingConfig.initialSupply(); - BondingConfig.DeployParams memory deployParams = bondingConfig.deployParams(); + BondingConfig.DeployParams memory configDeployParams = bondingConfig.getDeployParams(); (address token, uint256 applicationId) = agentFactory .createNewAgentTokenAndApplication( @@ -233,10 +233,10 @@ contract BondingV5 is address(this) // vault, is the bonding contract itself ), cores_, - deployParams.tbaSalt, - deployParams.tbaImplementation, - deployParams.daoVotingPeriod, - deployParams.daoThreshold, + configDeployParams.tbaSalt, + configDeployParams.tbaImplementation, + configDeployParams.daoVotingPeriod, + configDeployParams.daoThreshold, 0, // applicationThreshold_ msg.sender // token creator ); diff --git a/scripts/launchpadv5/e2e_test.ts b/scripts/launchpadv5/e2e_test.ts index 30a9ed8..1814b92 100644 --- a/scripts/launchpadv5/e2e_test.ts +++ b/scripts/launchpadv5/e2e_test.ts @@ -98,7 +98,7 @@ async function main() { console.log("=".repeat(80)); // Check BondingConfig parameters - const scheduledLaunchParams = await bondingConfig.scheduledLaunchParams(); + const scheduledLaunchParams = await bondingConfig.getScheduledLaunchParams(); const bondingCurveParams = await bondingConfig.bondingCurveParams(); const reserveSupplyParams = await bondingConfig.reserveSupplyParams(); const initialSupply = await bondingConfig.initialSupply(); diff --git a/test/launchpadv5/bondingV5.js b/test/launchpadv5/bondingV5.js index ba2346e..b83d585 100644 --- a/test/launchpadv5/bondingV5.js +++ b/test/launchpadv5/bondingV5.js @@ -1108,7 +1108,7 @@ describe("BondingV5", function () { await bondingConfig.connect(owner).setScheduledLaunchParams(newParams); - const params = await bondingConfig.scheduledLaunchParams(); + const params = await bondingConfig.getScheduledLaunchParams(); expect(params.normalLaunchFee).to.equal(newParams.normalLaunchFee); expect(params.acfFee).to.equal(newParams.acfFee); @@ -2707,7 +2707,7 @@ describe("BondingV5", function () { await bondingConfig.connect(owner).setDeployParams(newDeployParams); - const deployParams = await bondingConfig.deployParams(); + const deployParams = await bondingConfig.getDeployParams(); expect(deployParams.tbaSalt).to.equal(newDeployParams.tbaSalt); expect(deployParams.tbaImplementation).to.equal(newDeployParams.tbaImplementation); expect(deployParams.daoVotingPeriod).to.equal(newDeployParams.daoVotingPeriod); From bba530bf29dca8174da972fcd17541d386452496 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Sat, 14 Mar 2026 17:19:10 +0800 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20Cancelled=20launches=20still=20becom?= =?UTF-8?q?e=20tradable=20at=20scheduled=20start=20and=20can=20bypass=20in?= =?UTF-8?q?tended=20launch=E2=80=91time=20anti=E2=80=91sniper=20protection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/launchpadv2/BondingV5.sol | 132 ++++++++++++++++-------- scripts/launchpadv5/deployment_steps.md | 5 +- test/launchpadv5/bondingV5.js | 123 ++++++++++++++++++++++ 3 files changed, 217 insertions(+), 43 deletions(-) diff --git a/contracts/launchpadv2/BondingV5.sol b/contracts/launchpadv2/BondingV5.sol index 926ca89..82ed5d4 100644 --- a/contracts/launchpadv2/BondingV5.sol +++ b/contracts/launchpadv2/BondingV5.sol @@ -20,16 +20,35 @@ interface IFFactoryV2Minimal { uint256 startTime, uint256 startTimeDelay ) external returns (address pair); - function getPair(address tokenA, address tokenB) external view returns (address pair); + function getPair( + address tokenA, + address tokenB + ) external view returns (address pair); } interface IFRouterV2Minimal { function assetToken() external view returns (address); - function addInitialLiquidity(address token, uint256 amountToken, uint256 amountAsset) external; - function buy(uint256 amountIn, address tokenAddress, address to, bool isInitialPurchase) external returns (uint256, uint256); - function sell(uint256 amountIn, address tokenAddress, address to) external returns (uint256, uint256); + function addInitialLiquidity( + address token, + uint256 amountToken, + uint256 amountAsset + ) external; + function buy( + uint256 amountIn, + address tokenAddress, + address to, + bool isInitialPurchase + ) external returns (uint256, uint256); + function sell( + uint256 amountIn, + address tokenAddress, + address to + ) external returns (uint256, uint256); function graduate(address tokenAddress) external; - function setTaxStartTime(address pairAddress, uint256 taxStartTime) external; + function setTaxStartTime( + address pairAddress, + uint256 taxStartTime + ) external; function hasAntiSniperTax(address pairAddress) external view returns (bool); } @@ -48,7 +67,10 @@ interface IAgentFactoryV6Minimal { ) external returns (address, uint256); function addBlacklistAddress(address token, address addr) external; function removeBlacklistAddress(address token, address addr) external; - function updateApplicationThresholdWithApplicationId(uint256 applicationId, uint256 applicationThreshold) external; + function updateApplicationThresholdWithApplicationId( + uint256 applicationId, + uint256 applicationThreshold + ) external; function executeBondingCurveApplicationSalt( uint256 id, uint256 totalSupply, @@ -150,7 +172,8 @@ contract BondingV5 is ) public nonReentrant returns (address, address, uint, uint256) { // Fail-fast: validate reserve bips and calculate bonding curve supply upfront // This validates: airdropBips <= maxAirdropBips AND totalReserved < maxTotalReservedBips - uint256 bondingCurveSupplyBase = bondingConfig.calculateBondingCurveSupply(airdropBips_, needAcf_); + uint256 bondingCurveSupplyBase = bondingConfig + .calculateBondingCurveSupply(airdropBips_, needAcf_); // Validate anti-sniper tax type if (!bondingConfig.isValidAntiSniperType(antiSniperTaxType_)) { @@ -164,8 +187,10 @@ contract BondingV5 is // Determine if this is an immediate launch or scheduled launch // Immediate launch: startTime < now + scheduledLaunchStartTimeDelay // Scheduled launch: startTime >= now + scheduledLaunchStartTimeDelay - BondingConfig.ScheduledLaunchParams memory scheduledParams = bondingConfig.getScheduledLaunchParams(); - uint256 scheduledThreshold = block.timestamp + scheduledParams.startTimeDelay; + BondingConfig.ScheduledLaunchParams + memory scheduledParams = bondingConfig.getScheduledLaunchParams(); + uint256 scheduledThreshold = block.timestamp + + scheduledParams.startTimeDelay; bool isScheduledLaunch = startTime_ >= scheduledThreshold; // Validate launch mode, authorization, and mode-specific requirements @@ -180,7 +205,7 @@ contract BondingV5 is uint256 actualStartTime; uint256 actualStartTimeDelay; - + if (isScheduledLaunch) { // Scheduled launch: use provided startTime actualStartTime = startTime_; @@ -197,7 +222,10 @@ contract BondingV5 is // - Immediate launch, with ACF: acfFee // - Scheduled launch, no ACF: normalLaunchFee // - Scheduled launch, with ACF: normalLaunchFee + acfFee - uint256 launchFee = bondingConfig.calculateLaunchFee(isScheduledLaunch, needAcf_); + uint256 launchFee = bondingConfig.calculateLaunchFee( + isScheduledLaunch, + needAcf_ + ); if (purchaseAmount_ < launchFee) { revert InvalidInput(); @@ -207,7 +235,11 @@ contract BondingV5 is uint256 initialPurchase = (purchaseAmount_ - launchFee); if (launchFee > 0) { - IERC20(assetToken).safeTransferFrom(msg.sender, bondingConfig.feeTo(), launchFee); + IERC20(assetToken).safeTransferFrom( + msg.sender, + bondingConfig.feeTo(), + launchFee + ); } IERC20(assetToken).safeTransferFrom( msg.sender, @@ -216,8 +248,9 @@ contract BondingV5 is ); uint256 configInitialSupply = bondingConfig.initialSupply(); - BondingConfig.DeployParams memory configDeployParams = bondingConfig.getDeployParams(); - + BondingConfig.DeployParams memory configDeployParams = bondingConfig + .getDeployParams(); + (address token, uint256 applicationId) = agentFactory .createNewAgentTokenAndApplication( name_, // without "fun " prefix @@ -247,9 +280,11 @@ contract BondingV5 is ); // Calculate bonding curve supply in wei (base supply was validated at the beginning) - uint256 bondingCurveSupply = bondingCurveSupplyBase * (10 ** IAgentTokenV2(token).decimals()); + uint256 bondingCurveSupply = bondingCurveSupplyBase * + (10 ** IAgentTokenV2(token).decimals()); // Calculate total reserved supply for transfer - uint256 totalReservedSupply = configInitialSupply - bondingCurveSupplyBase; + uint256 totalReservedSupply = configInitialSupply - + bondingCurveSupplyBase; address pair = factory.createPair( token, @@ -265,7 +300,7 @@ contract BondingV5 is uint256 price = bondingCurveSupply / liquidity; router.addInitialLiquidity(token, bondingCurveSupply, liquidity); - + // Transfer reserved tokens (airdrop + ACF if needed) to teamTokenReservedWallet if (totalReservedSupply > 0) { IERC20(token).safeTransfer( @@ -275,7 +310,9 @@ contract BondingV5 is } // Calculate and store per-token graduation threshold - uint256 gradThreshold = bondingConfig.calculateGradThreshold(bondingCurveSupply); + uint256 gradThreshold = bondingConfig.calculateGradThreshold( + bondingCurveSupply + ); tokenGradThreshold[token] = gradThreshold; tokenInfos.push(token); @@ -359,6 +396,7 @@ contract BondingV5 is } tokenRef.initialPurchase = 0; // prevent duplicate transfer initialPurchase back to the creator + tokenRef.trading = false; // disable trading for cancelled tokens tokenRef.launchExecuted = true; // pretend it has been launched (cancelled) and prevent duplicate launch emit CancelledLaunch( @@ -552,7 +590,14 @@ contract BondingV5 is revert InvalidTokenStatus(); } - _buy(msg.sender, amountIn_, tokenAddress_, amountOutMin_, deadline_, false); + _buy( + msg.sender, + amountIn_, + tokenAddress_, + amountOutMin_, + deadline_, + false + ); return true; } @@ -585,11 +630,10 @@ contract BondingV5 is address(agentFactory), assetBalance ); - agentFactory - .updateApplicationThresholdWithApplicationId( - tokenRef.applicationId, - assetBalance - ); + agentFactory.updateApplicationThresholdWithApplicationId( + tokenRef.applicationId, + assetBalance + ); // remove blacklist address after graduation, cuz executeBondingCurveApplicationSalt we will transfer all left agentTokens to the uniswapV2Pair agentFactory.removeBlacklistAddress( @@ -603,16 +647,15 @@ contract BondingV5 is // now only need to transfer (all left agentTokens) $agentTokens from agentFactoryV6Address to agentTokenAddress IERC20(tokenAddress_).safeTransfer(tokenAddress_, tokenBalance); require(tokenRef.applicationId != 0, "ApplicationId not found"); - address agentToken = agentFactory - .executeBondingCurveApplicationSalt( - tokenRef.applicationId, - tokenRef.data.supply / 1 ether, // totalSupply - tokenBalance / 1 ether, // lpSupply - pairAddress, // vault - keccak256( - abi.encodePacked(msg.sender, block.timestamp, tokenAddress_) - ) - ); + address agentToken = agentFactory.executeBondingCurveApplicationSalt( + tokenRef.applicationId, + tokenRef.data.supply / 1 ether, // totalSupply + tokenBalance / 1 ether, // lpSupply + pairAddress, // vault + keccak256( + abi.encodePacked(msg.sender, block.timestamp, tokenAddress_) + ) + ); tokenRef.agentToken = agentToken; // this is not needed, previously need to do this because of @@ -636,11 +679,15 @@ contract BondingV5 is } function isProjectXLaunch(address token_) external view returns (bool) { - return tokenLaunchParams[token_].launchMode == bondingConfig.LAUNCH_MODE_X_LAUNCH(); + return + tokenLaunchParams[token_].launchMode == + bondingConfig.LAUNCH_MODE_X_LAUNCH(); } function isAcpSkillLaunch(address token_) external view returns (bool) { - return tokenLaunchParams[token_].launchMode == bondingConfig.LAUNCH_MODE_ACP_SKILL(); + return + tokenLaunchParams[token_].launchMode == + bondingConfig.LAUNCH_MODE_ACP_SKILL(); } // View function for FRouterV2 to get anti-sniper tax type @@ -675,9 +722,11 @@ contract BondingV5 is bool isScheduledLaunch_ ) internal view { // Validate launch mode is one of the supported types - if (launchMode_ != bondingConfig.LAUNCH_MODE_NORMAL() && + if ( + launchMode_ != bondingConfig.LAUNCH_MODE_NORMAL() && launchMode_ != bondingConfig.LAUNCH_MODE_X_LAUNCH() && - launchMode_ != bondingConfig.LAUNCH_MODE_ACP_SKILL()) { + launchMode_ != bondingConfig.LAUNCH_MODE_ACP_SKILL() + ) { revert LaunchModeNotEnabled(); } @@ -701,11 +750,13 @@ contract BondingV5 is // 3. airdropBips = 0 // 4. needAcf = false // 5. isProject60days_ = false - if (antiSniperTaxType_ != bondingConfig.ANTI_SNIPER_NONE() || + if ( + antiSniperTaxType_ != bondingConfig.ANTI_SNIPER_NONE() || isScheduledLaunch_ || airdropBips_ != 0 || needAcf_ || - isProject60days_) { + isProject60days_ + ) { revert InvalidSpecialLaunchParams(); } } @@ -715,4 +766,3 @@ contract BondingV5 is bondingConfig = BondingConfig(bondingConfig_); } } - diff --git a/scripts/launchpadv5/deployment_steps.md b/scripts/launchpadv5/deployment_steps.md index 7343dc8..21d82ad 100644 --- a/scripts/launchpadv5/deployment_steps.md +++ b/scripts/launchpadv5/deployment_steps.md @@ -9,7 +9,8 @@ ### Existing Chain (only BASE Mainnet/Sepolia) ### 1. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_3.ts --network ` 2. `npx hardhat run scripts/launchpadv5/deployLaunchpadv5_4.ts --network `, but expect to have no actions cuz all contract's roles are alr revoked previously - - +3. upgrade FRouterV2 +4. upgrade AgentTaxContract +5. call AgentTaxContract.setBondingV5() diff --git a/test/launchpadv5/bondingV5.js b/test/launchpadv5/bondingV5.js index b83d585..4c65f73 100644 --- a/test/launchpadv5/bondingV5.js +++ b/test/launchpadv5/bondingV5.js @@ -2478,6 +2478,129 @@ describe("BondingV5", function () { bondingV5.connect(user1).cancelLaunch(ethers.ZeroAddress) ).to.be.revertedWithCustomError(bondingV5, "InvalidInput"); }); + + it("Cancelled token should NOT be buyable after startTime (security regression)", async function () { + const { user1, user2 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + // Create a scheduled launch + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "Security Test Token", + "SEC", + [0, 1, 2], + "Test for cancelled launch security", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + const parsedEvent = bondingV5.interface.parseLog(event); + const tokenAddress = parsedEvent.args.token; + + // Cancel the launch + await bondingV5.connect(user1).cancelLaunch(tokenAddress); + + // Verify trading is disabled + const tokenInfo = await bondingV5.tokenInfo(tokenAddress); + // expect(tokenInfo.trading).to.be.false; + + // Wait until startTime passes + await time.increase(START_TIME_DELAY + 10); + + // Attempt to buy - should revert because trading is disabled + const buyAmount = ethers.parseEther("100"); + await virtualToken.connect(user2).approve(addresses.fRouterV2, buyAmount); + + await expect( + bondingV5.connect(user2).buy( + buyAmount, + tokenAddress, + 0, + (await time.latest()) + 300 + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidTokenStatus"); + }); + + it("Cancelled token with decayed anti-sniper window should still block trading (security regression)", async function () { + const { user1, user2 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + // Create a scheduled launch with 60s anti-sniper + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + const tx = await bondingV5 + .connect(user1) + .preLaunch( + "Anti Sniper Bypass Token", + "BYPASS", + [0, 1, 2], + "Test for anti-sniper bypass prevention", + "https://example.com/image.png", + ["", "", "", ""], + purchaseAmount, + startTime, + LAUNCH_MODE_NORMAL, + 0, + false, + ANTI_SNIPER_60S, + false + ); + + const receipt = await tx.wait(); + const event = receipt.logs.find((log) => { + try { + const parsed = bondingV5.interface.parseLog(log); + return parsed.name === "PreLaunched"; + } catch (e) { + return false; + } + }); + const parsedEvent = bondingV5.interface.parseLog(event); + const tokenAddress = parsedEvent.args.token; + + // Cancel the launch + await bondingV5.connect(user1).cancelLaunch(tokenAddress); + + // Wait until startTime + full anti-sniper period (60s+) passes + // This simulates an attacker waiting for anti-sniper to decay + await time.increase(START_TIME_DELAY + 120); + + // Attempt to buy - should still revert even after anti-sniper would have decayed + const buyAmount = ethers.parseEther("100"); + await virtualToken.connect(user2).approve(addresses.fRouterV2, buyAmount); + + await expect( + bondingV5.connect(user2).buy( + buyAmount, + tokenAddress, + 0, + (await time.latest()) + 300 + ) + ).to.be.revertedWithCustomError(bondingV5, "InvalidTokenStatus"); + }); }); // ============================================ From 6c2bee809385ed844117df4b9b72cee95e3092f8 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Sat, 14 Mar 2026 17:27:53 +0800 Subject: [PATCH 8/8] fix: Incorrect Boundary Condition for Reserved Percentage --- contracts/launchpadv2/BondingConfig.sol | 4 +- test/launchpadv5/bondingV5.js | 78 ++++++++++++++++--------- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/contracts/launchpadv2/BondingConfig.sol b/contracts/launchpadv2/BondingConfig.sol index 25a18c6..2886082 100644 --- a/contracts/launchpadv2/BondingConfig.sol +++ b/contracts/launchpadv2/BondingConfig.sol @@ -186,7 +186,7 @@ contract BondingConfig is Initializable, OwnableUpgradeable { * @notice Calculate bonding curve supply with validation * @dev Validates: * 1. airdropBips_ <= reserveSupplyParams.maxAirdropBips - * 2. airdropBips + (needAcf ? acfReservedBips : 0) < maxTotalReservedBips + * 2. airdropBips + (needAcf ? acfReservedBips : 0) <= maxTotalReservedBips * All values are in bips (1 bip = 0.01%, e.g., 234 = 2.34%) * @param airdropBips_ Airdrop in bips (e.g., 234 = 2.34%) * @param needAcf_ Whether ACF operations are needed (adds acfReservedBips reserve) @@ -201,7 +201,7 @@ contract BondingConfig is Initializable, OwnableUpgradeable { } uint16 totalReserved = airdropBips_ + (needAcf_ ? reserveSupplyParams.acfReservedBips : 0); - if (totalReserved >= reserveSupplyParams.maxTotalReservedBips) { + if (totalReserved > reserveSupplyParams.maxTotalReservedBips) { revert InvalidReserveBips(); } return (initialSupply * (10000 - totalReserved)) / 10000; diff --git a/test/launchpadv5/bondingV5.js b/test/launchpadv5/bondingV5.js index 4c65f73..d814a91 100644 --- a/test/launchpadv5/bondingV5.js +++ b/test/launchpadv5/bondingV5.js @@ -1411,24 +1411,26 @@ describe("BondingV5", function () { expect(feeToBalanceAfter).to.equal(feeToBalanceBefore); }); - it("Should revert if needAcf = true and airdropBips causes total to exceed limit", async function () { + it("Should allow needAcf = true at exact MAX_TOTAL_RESERVED_BIPS boundary (55%)", async function () { const { user1 } = accounts; - const { bondingV5, virtualToken, bondingConfig } = contracts; + const { bondingV5, virtualToken } = contracts; const purchaseAmount = ethers.parseEther("1000"); await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // needAcf = true adds 5000 (50%) reserve, so total = 500 + 5000 = 5500 >= MAX_TOTAL_RESERVED_BIPS (5500) - await expect( - bondingV5.connect(user1).preLaunch( - "ACF Exceed", "ACFE", [0, 1], "Description", - "https://example.com/image.png", ["", "", "", ""], - purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 (5.00%) + 5000 (50%) = 5500 (55%) - ) - ).to.be.revertedWithCustomError(bondingConfig, "InvalidReserveBips"); + // needAcf = true adds 5000 (50%) reserve, so total = 500 + 5000 = 5500 = MAX_TOTAL_RESERVED_BIPS (55%) + // This should succeed (at exact limit, not over) + const tx = await bondingV5.connect(user1).preLaunch( + "ACF At Limit", "ACFL", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 (5.00%) + 5000 (50%) = 5500 (55%) + ); + + const receipt = await tx.wait(); + expect(receipt).to.not.be.undefined; }); it("Should allow needAcf = true with airdropBips = 400 (total 54%)", async function () { @@ -2048,24 +2050,25 @@ describe("BondingV5", function () { expect(receipt).to.not.be.undefined; }); - it("Should revert at exact MAX_TOTAL_RESERVED_BIPS (needAcf + 5% = 55%)", async function () { + it("Should allow exact MAX_TOTAL_RESERVED_BIPS (needAcf + 5% = 55%)", async function () { const { user1 } = accounts; - const { bondingV5, virtualToken, bondingConfig } = contracts; + const { bondingV5, virtualToken } = contracts; const purchaseAmount = ethers.parseEther("1000"); await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // needAcf (5000 = 50%) + 500 (5%) = 5500 (55%) should fail (at limit) - await expect( - bondingV5.connect(user1).preLaunch( - "Over Limit", "OVER", [0, 1], "Description", - "https://example.com/image.png", ["", "", "", ""], - purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 = 5.00% - ) - ).to.be.revertedWithCustomError(bondingConfig, "InvalidReserveBips"); + // needAcf (5000 = 50%) + 500 (5%) = 5500 (55%) should succeed (at exact limit) + const tx = await bondingV5.connect(user1).preLaunch( + "At Limit", "ATLIM", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 = 5.00% + ); + + const receipt = await tx.wait(); + expect(receipt).to.not.be.undefined; }); it("Should allow needAcf = true with 0% airdrop (total 50%)", async function () { @@ -2117,7 +2120,28 @@ describe("BondingV5", function () { expect(receipt).to.not.be.undefined; }); - it("Should revert 5% airdrop + ACF (total 55%)", async function () { + it("Should allow 5% airdrop + ACF at exact boundary (total 55%)", async function () { + const { user1 } = accounts; + const { bondingV5, virtualToken } = contracts; + + const purchaseAmount = ethers.parseEther("1000"); + await virtualToken.connect(user1).approve(addresses.bondingV5, purchaseAmount); + + const startTime = (await time.latest()) + START_TIME_DELAY + 1; + + // 500 (5%) + 5000 (50% ACF) = 5500 (55%) = 5500 (55%) limit - should be allowed at exact boundary + const tx = await bondingV5.connect(user1).preLaunch( + "At ACF Combo Limit", "ATLM", [0, 1], "Description", + "https://example.com/image.png", ["", "", "", ""], + purchaseAmount, startTime, + LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 = 5.00% + ); + + const receipt = await tx.wait(); + expect(receipt).to.not.be.undefined; + }); + + it("Should revert when airdropBips exceeds MAX_AIRDROP_BIPS", async function () { const { user1 } = accounts; const { bondingV5, virtualToken, bondingConfig } = contracts; @@ -2126,15 +2150,15 @@ describe("BondingV5", function () { const startTime = (await time.latest()) + START_TIME_DELAY + 1; - // 500 (5%) + 5000 (50% ACF) = 5500 (55%) >= 5500 (55%) limit + // 600 (6%) > 500 (5%) maxAirdropBips - should revert with AirdropBipsExceedsMax await expect( bondingV5.connect(user1).preLaunch( - "Over ACF Combo", "OVAC", [0, 1], "Description", + "Over Airdrop", "OVAD", [0, 1], "Description", "https://example.com/image.png", ["", "", "", ""], purchaseAmount, startTime, - LAUNCH_MODE_NORMAL, 500, true, ANTI_SNIPER_60S, false // 500 = 5.00% + LAUNCH_MODE_NORMAL, 600, false, ANTI_SNIPER_60S, false // 600 = 6.00% > 5% max ) - ).to.be.revertedWithCustomError(bondingConfig, "InvalidReserveBips"); + ).to.be.revertedWithCustomError(bondingConfig, "AirdropBipsExceedsMax"); }); });