GreenTownLaunchpad

0xe312e68d45790a1315155ae9bfa85072d10f2543

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
21,551 bytes
ABI entries
88
38 read · 23 write
License
none

Contract information

Address
0xe312e68d45790a1315155ae9bfa85072d10f2543
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xf8b7f06d8c…27a61d9092

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (38)

AUDIENCE_CREATOR_KEY_HOLDERS()uint8
AUDIENCE_HOUSE_RESIDENTS()uint8
AUDIENCE_KEYS_HELD_BY_CREATOR()uint8
BPS()uint256
GRADUATION_ETH_TOLERANCE()uint256
LP_SUPPLY()uint256
TOTAL_SUPPLY()uint256
VIRTUAL_TOKEN_SEED()uint256
airdropVault()address
allTokens(uint256)address
allTradingPaused()bool
authorizedSigner()address
buyRestrictedUntil(address)uint48
buyRestrictionAudiences(address)uint8
buysPaused()bool
citizenFees(address)uint256
citizenOf(address)address
citizenShareBps()uint16
creationPaused()bool
curves(address)uint128, uint128, uint128, uint128, address, bool, bool, bool
graduationEthTarget(address)uint128
launchFee()uint256
migrator()address
officialPool(address)address
officialPoolId(address)bytes32
owner()address
protocolFees()uint256
quoteBuy(address, uint256)uint256, uint256
referralFees(address)uint256
referralShareBps()uint16
tokenCount()uint256
tokenNonce()uint256
tradeFeeBps()uint16
treasury()address
treasuryFees()uint256
usedBuyAuth(bytes32)bool
userToReferrer(address)address
virtualEthSeed()uint128

Events (25)

AirdropVaultSetAuthorizedSignerSetBuyRestrictionSetCitizenFeesClaimedCitizenShareSetGraduatedLaunchAirdropCreatedLaunchFeeSetMigratedOwnershipTransferredPauseStateSetPoolPreparedProtocolFeesWithdrawnReferralFeesClaimedReferralSetReferralShareSetTokenCreatedTokenTradingPauseSetTradeTradeFeeSetTreasuryFeesWithdrawnTreasurySetV4MigratedV4PoolPreparedVirtualEthSeedSet

ABI

[
  {
    "inputs": [
      {
        "internalType": "uint128",
        "name": "virtualEthSeed_",
        "type": "uint128"
      },
      {
        "internalType": "uint16",
        "name": "tradeFeeBps_",
        "type": "uint16"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      }
    ],
    "name": "AirdropVaultSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "newSigner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousSigner",
        "type": "address"
      }
    ],
    "name": "AuthorizedSignerSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint48",
        "name": "restrictedUntil",
        "type": "uint48"
      },
      {
        "indexed": false,
        "internalType": "uint32",
        "name": "durationSeconds",
        "type": "uint32"
      },
      {
        "indexed": false,
        "internalType": "uint8",
        "name": "audiences",
        "type": "uint8"
      }
    ],
    "name": "BuyRestrictionSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "citizen",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "CitizenFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "citizenShareBps",
        "type": "uint16"
      }
    ],
    "name": "CitizenShareSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "raisedEth",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "campaignId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethSpent",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "LaunchAirdropCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "launchFee",
        "type": "uint256"
      }
    ],
    "name": "LaunchFeeSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethLiquidity",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenLiquidity",
        "type": "uint256"
      }
    ],
    "name": "Migrated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bool",
        "name": "creationPaused",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "buysPaused",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "allTradingPaused",
        "type": "bool"
      }
    ],
    "name": "PauseStateSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "expectedEthLiquidity",
        "type": "uint256"
      }
    ],
    "name": "PoolPrepared",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ProtocolFeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ReferralFeesClaimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      }
    ],
    "name": "ReferralSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "referralShareBps",
        "type": "uint16"
      }
    ],
    "name": "ReferralShareSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "citizen",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "curveSupply",
        "type": "uint256"
      }
    ],
    "name": "TokenCreated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "TokenTradingPauseSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "trader",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "isBuy",
        "type": "bool"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "name": "Trade",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint16",
        "name": "tradeFeeBps",
        "type": "uint16"
      }
    ],
    "name": "TradeFeeSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "TreasuryFeesWithdrawn",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      }
    ],
    "name": "TreasurySet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "poolManager",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethLiquidity",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenLiquidity",
        "type": "uint256"
      }
    ],
    "name": "V4Migrated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "bytes32",
        "name": "poolId",
        "type": "bytes32"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "poolManager",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "expectedEthLiquidity",
        "type": "uint256"
      }
    ],
    "name": "V4PoolPrepared",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "virtualEthSeed",
        "type": "uint128"
      }
    ],
    "name": "VirtualEthSeedSet",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "AUDIENCE_CREATOR_KEY_HOLDERS",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "AUDIENCE_HOUSE_RESIDENTS",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "AUDIENCE_KEYS_HELD_BY_CREATOR",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "GRADUATION_ETH_TOLERANCE",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "LP_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOTAL_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "VIRTUAL_TOKEN_SEED",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "airdropVault",
    "outputs": [
      {
        "internalType": "contract IAirdropVault",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "allTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "allTradingPaused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "authorizedSigner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "buyRestrictedUntil",
    "outputs": [
      {
        "internalType": "uint48",
        "name": "",
        "type": "uint48"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "buyRestrictionAudiences",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "maxEth",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "nonce",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "signature",
        "type": "bytes"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      }
    ],
    "name": "buySigned",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "buysPaused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "citizenFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "citizenOf",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "citizenShareBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "claimCitizenFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimReferralFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      }
    ],
    "name": "createToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "minTokensOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "referrer",
        "type": "address"
      },
      {
        "internalType": "uint32",
        "name": "restrictedFor",
        "type": "uint32"
      },
      {
        "internalType": "uint8",
        "name": "audiences",
        "type": "uint8"
      }
    ],
    "name": "createTokenRestricted",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "creatorBuyEth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minCreatorTokensOut",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "airdropBuyEth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minAirdropTokensOut",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "referrer",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "merkleRoot",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "totalWeight",
            "type": "uint256"
          },
          {
            "internalType": "uint48",
            "name": "snapshotBlock",
            "type": "uint48"
          },
          {
            "internalType": "uint48",
            "name": "expiresAt",
            "type": "uint48"
          }
        ],
        "internalType": "struct GreenTownLaunchpad.LaunchAirdropConfig",
        "name": "config",
        "type": "tuple"
      }
    ],
    "name": "createTokenWithAirdrop",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "campaignId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "symbol",
        "type": "string"
      },
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "creatorBuyEth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minCreatorTokensOut",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "airdropBuyEth",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minAirdropTokensOut",
            "type": "uint256"
          },
          {
            "internalType": "address",
            "name": "referrer",
            "type": "address"
          },
          {
            "internalType": "bytes32",
            "name": "merkleRoot",
            "type": "bytes32"
          },
          {
            "internalType": "uint256",
            "name": "totalWeight",
            "type": "uint256"
          },
          {
            "internalType": "uint48",
            "name": "snapshotBlock",
            "type": "uint48"
          },
          {
            "internalType": "uint48",
            "name": "expiresAt",
            "type": "uint48"
          }
        ],
        "internalType": "struct GreenTownLaunchpad.LaunchAirdropConfig",
        "name": "config",
        "type": "tuple"
      },
      {
        "internalType": "uint32",
        "name": "restrictedFor",
        "type": "uint32"
      },
      {
        "internalType": "uint8",
        "name": "audiences",
        "type": "uint8"
      }
    ],
    "name": "createTokenWithAirdropRestricted",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "campaignId",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "creationPaused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "curves",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "virtualEth",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "virtualTokens",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "realEth",
        "type": "uint128"
      },
      {
        "internalType": "uint128",
        "name": "realTokens",
        "type": "uint128"
      },
      {
        "internalType": "address",
        "name": "citizen",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "graduated",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "migrated",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "tradingPaused",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "graduationEthTarget",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchFee",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "migrator",
    "outputs": [
      {
        "internalType": "contract IMigrator",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "officialPool",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "officialPoolId",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "protocolFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "ethIn",
        "type": "uint256"
      }
    ],
    "name": "quoteBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "tokensOut",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "fee",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "referralFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "referralShareBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      }
    ],
    "name": "sell",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "value",
        "type": "address"
      }
    ],
    "name": "setAirdropVault",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "signer",
        "type": "address"
      }
    ],
    "name": "setAuthorizedSigner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "value",
        "type": "uint16"
      }
    ],
    "name": "setCitizenShareBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "setLaunchFee",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "value",
        "type": "address"
      }
    ],
    "name": "setMigrator",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "creation",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "buys",
        "type": "bool"
      },
      {
        "internalType": "bool",
        "name": "allTrading",
        "type": "bool"
      }
    ],
    "name": "setPauseState",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "value",
        "type": "uint16"
      }
    ],
    "name": "setReferralShareBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "paused",
        "type": "bool"
      }
    ],
    "name": "setTokenTradingPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint16",
        "name": "value",
        "type": "uint16"
      }
    ],
    "name": "setTradeFeeBps",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "value",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint128",
        "name": "value",
        "type": "uint128"
      }
    ],
    "name": "setVirtualEthSeed",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tokenNonce",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "tradeFeeBps",
    "outputs": [
      {
        "internalType": "uint16",
        "name": "",
        "type": "uint16"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasuryFees",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "name": "usedBuyAuth",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "userToReferrer",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "virtualEthSeed",
    "outputs": [
      {
        "internalType": "uint128",
        "name": "",
        "type": "uint128"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "withdrawProtocolFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "withdrawTreasuryFees",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {GreenTownToken} from "./GreenTownToken.sol";

/// @notice Minimal interface for initializing the gated canonical v4 pool before
///         trading and seeding it atomically when the curve completes.
interface IMigrator {
    function prepare(address token, uint256 tokenAmount, uint256 ethAmount)
        external returns (address poolManager, bytes32 poolId);
    function migrate(address token, uint256 tokenAmount)
        external payable returns (address poolManager, bytes32 poolId, uint256 tokenUsed, uint256 ethUsed);
}

interface IAirdropVault {
    function registerLaunchCampaign(
        address token,
        address creator,
        uint256 amount,
        bytes32 merkleRoot,
        uint256 totalWeight,
        uint48 snapshotBlock,
        uint48 expiresAt
    ) external returns (uint256 campaignId);
}

/// @title  GreenTownLaunchpad — GreenTown fair-launch bonding curve
/// @notice Lets anyone launch a token on a fair, transparent price curve. Buyers
///         trade against a "virtual reserve" constant-product curve (the same
///         x*y=k idea Uniswap uses), so the price rises smoothly with demand and
///         nobody gets a hidden pre-mine. Once the whole curve is bought out, the
///         token "graduates": its remaining supply plus all the ETH raised are
///         moved into a locked DEX pool so it can trade freely forever.
///
/// @dev    How the pieces fit together:
///          - Each token has a `Curve` holding its virtual + real reserves.
///          - `citizen` is the person who launched the token (its creator).
///          - A small trade fee is split between that citizen and the protocol.
///
///         Safety notes (why funds stay safe):
///          - Every function that moves ETH is `nonReentrant` and follows the
///            checks-effects-interactions pattern: we update all bookkeeping
///            BEFORE sending any ETH, so a re-entrant call sees final state.
///          - `migrator` is set exactly once and then locked.
///          - Owner controls (pause switches, fee tuning) are bounded — e.g. the
///            trade fee can never exceed 5%.
contract GreenTownLaunchpad is Ownable, ReentrancyGuard {
    using ECDSA for bytes32;

    // ---------------------------------------------------------------------
    // Fixed token economics (identical for every launch)
    // ---------------------------------------------------------------------

    /// @notice Every launched token mints exactly this many units.
    uint256 public constant TOTAL_SUPPLY = 1_000_000_000e18; // 1B tokens
    /// @notice Supply held back and paired with the raised ETH as DEX liquidity at graduation.
    uint256 public constant LP_SUPPLY = 200_000_000e18; // 200M (20%)
    /// @notice Virtual token reserve seed — sets the curve's starting shape/price.
    uint256 public constant VIRTUAL_TOKEN_SEED = 1_145_000_000e18; // 1.145B
    /// @notice Basis-points denominator (100% = 10_000).
    uint256 public constant BPS = 10_000;
    /// @notice Maximum path-dependent integer-rounding variance at graduation.
    uint256 public constant GRADUATION_ETH_TOLERANCE = 1 gwei;
    uint8 public constant AUDIENCE_CREATOR_KEY_HOLDERS = 1;
    uint8 public constant AUDIENCE_KEYS_HELD_BY_CREATOR = 2;
    uint8 public constant AUDIENCE_HOUSE_RESIDENTS = 4;
    uint8 private constant ALL_BUY_AUDIENCES = 7;

    /// @dev Per-token state. Reserves are the constant-product curve; "virtual"
    ///      reserves seed a sensible starting price, "real" reserves track what's
    ///      actually held by the curve.
    struct Curve {
        uint128 virtualEth;      // curve's virtual ETH reserve
        uint128 virtualTokens;   // curve's virtual token reserve
        uint128 realEth;         // ETH actually accumulated in the curve
        uint128 realTokens;      // tokens still available to buy on the curve
        address citizen;         // who launched this token
        bool graduated;          // curve sold out and moved to a DEX
        bool migrated;           // liquidity has been seeded on the DEX
        bool tradingPaused;      // per-token pause switch
    }

    struct LaunchAirdropConfig {
        uint256 creatorBuyEth;
        uint256 minCreatorTokensOut;
        uint256 airdropBuyEth;
        uint256 minAirdropTokensOut;
        address referrer;
        bytes32 merkleRoot;
        uint256 totalWeight;
        uint48 snapshotBlock;
        uint48 expiresAt;
    }

    // ---------------------------------------------------------------------
    // Configuration
    // ---------------------------------------------------------------------

    /// @notice Virtual ETH reserve seed — the entry-price / graduation knob.
    ///         Owner-tunable (see setVirtualEthSeed). Each token snapshots the
    ///         current value at creation, so changes only affect FUTURE launches;
    ///         tokens already on their curve keep the target they launched with.
    uint128 public virtualEthSeed;

    /// @notice Trade fee in bps, and the citizen's share of that fee in bps of the
    ///         fee. Both are owner-tunable at any time so the model can adapt —
    ///         the fee itself is hard-capped at 5% for buyer protection.
    ///         Default split: 10% to the citizen, 90% to the protocol.
    uint16 public tradeFeeBps;
    uint16 public citizenShareBps;

    /// @notice Set-once collaborators (locked after the first assignment).
    IMigrator public migrator;
    IAirdropVault public airdropVault;

    /// @notice Pause switches. `creationPaused` blocks new launches; `buysPaused`
    ///         blocks buys; `allTradingPaused` blocks buys and sells everywhere.
    bool public creationPaused;
    bool public buysPaused;
    bool public allTradingPaused;

    /// @notice Live curve for each token, and the fees each side has earned.
    mapping(address => Curve) public curves;
    /// @notice Compatibility venue address. In v4 every pool lives inside the
    ///         singleton PoolManager, while `officialPoolId` identifies the pool.
    mapping(address => address) public officialPool;
    mapping(address => bytes32) public officialPoolId;
    mapping(address => uint128) public graduationEthTarget;
    mapping(address => uint48) public buyRestrictedUntil;
    mapping(address => uint8) public buyRestrictionAudiences;
    mapping(address => uint256) public citizenFees; // claimable by each token's citizen
    uint256 public protocolFees;                    // claimable by the owner
    address[] public allTokens;
    uint256 public tokenNonce;

    /// @notice Referral: a share of every trade fee is routed to the trader's
    ///         referrer (like the keys contract). Sticky — the first referrer
    ///         recorded for a user wins. No referrer → that share stays with the
    ///         protocol. Share is owner-tunable via setReferralShareBps.
    mapping(address => uint256) public referralFees;   // claimable by each referrer
    mapping(address => address) public userToReferrer; // trader => referrer (sticky)
    uint16 public referralShareBps;

    /// @notice Flat fee (in wei) charged on every token launch and routed to the
    ///         treasury. Denominated in ETH but sized off-chain to a USD target
    ///         (e.g. ~$5); the owner updates it via setLaunchFee as ETH moves, and
    ///         can re-point `treasury` anytime. Accrues to `treasuryFees` and is
    ///         swept to `treasury` by withdrawTreasuryFees().
    address public treasury;
    uint256 public launchFee;     // wei required to launch a token
    uint256 public treasuryFees;  // accrued launch fees, withdrawable to treasury

    // ---------------------------------------------------------------------
    // Buy authorization (EIP-712 signed by an off-chain authorizer)
    // ---------------------------------------------------------------------
    // While authorizedSigner == address(0), enforcement is OFF and open buys work
    // exactly as normal. Once the owner sets a signer, open buys are disabled and
    // every buy must carry a fresh signature from that signer — so all the "who
    // can buy" logic (app-only, key-holder gating, launch windows, ...) lives in
    // your backend and can change anytime without touching this contract.
    address public authorizedSigner;
    mapping(bytes32 => bool) public usedBuyAuth; // each signature digest is single-use

    bytes32 private constant EIP712_DOMAIN_TYPEHASH =
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 private constant BUY_AUTH_TYPEHASH =
        keccak256("BuyAuth(address buyer,address token,uint256 maxEth,uint256 deadline,uint256 nonce)");

    // ---------------------------------------------------------------------
    // Events
    // ---------------------------------------------------------------------

    event TokenCreated(address indexed token, address indexed citizen, string name, string symbol, uint256 curveSupply);
    event BuyRestrictionSet(
        address indexed token,
        uint48 restrictedUntil,
        uint32 durationSeconds,
        uint8 audiences
    );
    event PoolPrepared(address indexed token, address indexed pool, uint256 expectedEthLiquidity);
    event V4PoolPrepared(
        address indexed token,
        bytes32 indexed poolId,
        address indexed poolManager,
        uint256 expectedEthLiquidity
    );
    event Trade(address indexed token, address indexed trader, bool isBuy, uint256 ethAmount, uint256 tokenAmount, uint256 fee);
    event Graduated(address indexed token, uint256 raisedEth);
    event Migrated(address indexed token, address indexed pool, uint256 ethLiquidity, uint256 tokenLiquidity);
    event V4Migrated(
        address indexed token,
        bytes32 indexed poolId,
        address indexed poolManager,
        uint256 ethLiquidity,
        uint256 tokenLiquidity
    );
    event PauseStateSet(bool creationPaused, bool buysPaused, bool allTradingPaused);
    event TokenTradingPauseSet(address indexed token, bool paused);
    event TradeFeeSet(uint16 tradeFeeBps);
    event CitizenShareSet(uint16 citizenShareBps);
    event CitizenFeesClaimed(address indexed token, address indexed citizen, uint256 amount);
    event ProtocolFeesWithdrawn(address indexed to, uint256 amount);
    event VirtualEthSeedSet(uint128 virtualEthSeed);
    event AuthorizedSignerSet(address indexed newSigner, address indexed previousSigner);
    event ReferralShareSet(uint16 referralShareBps);
    event ReferralSet(address indexed user, address indexed referrer);
    event ReferralFeesClaimed(address indexed referrer, uint256 amount);
    event TreasurySet(address indexed treasury);
    event LaunchFeeSet(uint256 launchFee);
    event TreasuryFeesWithdrawn(address indexed to, uint256 amount);
    event AirdropVaultSet(address indexed vault);
    event LaunchAirdropCreated(
        address indexed token,
        uint256 indexed campaignId,
        uint256 ethSpent,
        uint256 tokenAmount
    );

    /// @param virtualEthSeed_ Virtual ETH reserve seed (entry price). Must be non-zero.
    /// @param tradeFeeBps_    Starting trade fee in bps (<= 500, i.e. 5%).
    constructor(uint128 virtualEthSeed_, uint16 tradeFeeBps_) {
        require(virtualEthSeed_ != 0 && tradeFeeBps_ <= 500, "BAD_CONFIG");
        virtualEthSeed = virtualEthSeed_;
        tradeFeeBps = tradeFeeBps_;
        citizenShareBps = 1_000;  // 10% of each trade fee to the token's citizen
        referralShareBps = 500;   // 5% to the trader's referrer; protocol keeps the rest (85%)
        treasury = msg.sender;    // launch fees accrue here until re-pointed
    }

    // ---------------------------------------------------------------------
    // Owner configuration
    // ---------------------------------------------------------------------

    /// @notice Wire up the DEX migrator. Can be set only once, then it's locked in.
    function setMigrator(address value) external onlyOwner {
        require(
            address(migrator) == address(0) && value != address(0) && value.code.length != 0,
            "MIGRATOR_LOCKED"
        );
        migrator = IMigrator(value);
    }

    /// @notice Wire up the immutable campaign vault used by all future tokens.
    function setAirdropVault(address value) external onlyOwner {
        require(
            address(airdropVault) == address(0) && value != address(0) && value.code.length != 0,
            "AIRDROP_VAULT_LOCKED"
        );
        airdropVault = IAirdropVault(value);
        emit AirdropVaultSet(value);
    }

    /// @notice Flip the global pause switches (creation / buys / all trading).
    function setPauseState(bool creation, bool buys, bool allTrading) external onlyOwner {
        creationPaused = creation; buysPaused = buys; allTradingPaused = allTrading;
        emit PauseStateSet(creation, buys, allTrading);
    }

    /// @notice Pause or resume trading for a single token.
    function setTokenTradingPaused(address token, bool paused) external onlyOwner {
        require(curves[token].citizen != address(0), "UNKNOWN_TOKEN");
        curves[token].tradingPaused = paused;
        emit TokenTradingPauseSet(token, paused);
    }

    /// @notice Update the trade fee (bps). Capped at 5% so buyers are always protected.
    function setTradeFeeBps(uint16 value) external onlyOwner {
        require(value <= 500, "FEE_TOO_HIGH");
        tradeFeeBps = value;
        emit TradeFeeSet(value);
    }

    /// @notice Update the citizen's share of each trade fee (bps of the fee); the
    ///         remainder always goes to the protocol.
    function setCitizenShareBps(uint16 value) external onlyOwner {
        require(uint256(value) + referralShareBps <= BPS, "BAD_SHARE");
        citizenShareBps = value;
        emit CitizenShareSet(value);
    }

    /// @notice Update the referrer's share of each trade fee (bps of the fee). The
    ///         protocol keeps whatever remains after the citizen + referrer cuts.
    function setReferralShareBps(uint16 value) external onlyOwner {
        require(uint256(value) + citizenShareBps <= BPS, "BAD_SHARE");
        referralShareBps = value;
        emit ReferralShareSet(value);
    }

    /// @notice Tune the virtual ETH seed — i.e. where future tokens graduate. A
    ///         higher seed means more ETH is needed to bond and a higher
    ///         graduation market cap. Only affects tokens launched after this call.
    function setVirtualEthSeed(uint128 value) external onlyOwner {
        require(value != 0, "ZERO_SEED");
        virtualEthSeed = value;
        emit VirtualEthSeedSet(value);
    }

    /// @notice Re-point the treasury that launch fees accrue to. Never zero.
    ///         Only affects future withdrawals; already-accrued fees go to whatever
    ///         treasury is set when withdrawTreasuryFees() runs.
    function setTreasury(address value) external onlyOwner {
        require(value != address(0), "ZERO_ADDRESS");
        treasury = value;
        emit TreasurySet(value);
    }

    /// @notice Set the flat launch fee (wei). Sized off-chain to a USD target and
    ///         updated as ETH moves. Capped at 1 ETH so a fat-finger can't brick
    ///         launches.
    function setLaunchFee(uint256 value) external onlyOwner {
        require(value <= 1 ether, "FEE_TOO_HIGH");
        launchFee = value;
        emit LaunchFeeSet(value);
    }

    /// @notice Set (or clear) the off-chain buy authorizer. address(0) keeps buys
    ///         open; any other address turns on mandatory per-buy signatures.
    function setAuthorizedSigner(address signer) external onlyOwner {
        emit AuthorizedSignerSet(signer, authorizedSigner);
        authorizedSigner = signer;
    }

    // ---------------------------------------------------------------------
    // Fee claims
    // ---------------------------------------------------------------------

    /// @notice A token's citizen withdraws the trade fees their token has earned.
    /// @dev    Reentrancy-safe: `nonReentrant` plus we zero the balance BEFORE the
    ///         transfer, so even a re-entrant recipient can't double-claim.
    function claimCitizenFees(address token) external nonReentrant {
        address citizen = curves[token].citizen;
        require(citizen != address(0), "UNKNOWN_TOKEN");
        require(msg.sender == citizen, "NOT_CITIZEN");
        uint256 amount = citizenFees[token];
        require(amount != 0, "NO_FEES");
        citizenFees[token] = 0; // effect before interaction
        (bool ok,) = citizen.call{value: amount}("");
        require(ok, "ETH_TRANSFER_FAILED");
        emit CitizenFeesClaimed(token, citizen, amount);
    }

    /// @notice Owner withdraws the protocol's accumulated fees.
    /// @dev    Same reentrancy-safe pattern: balance is zeroed before the transfer.
    function withdrawProtocolFees(address to) external onlyOwner nonReentrant {
        require(to != address(0), "ZERO_ADDRESS");
        uint256 amount = protocolFees;
        require(amount != 0, "NO_FEES");
        protocolFees = 0; // effect before interaction
        (bool ok,) = to.call{value: amount}("");
        require(ok, "ETH_TRANSFER_FAILED");
        emit ProtocolFeesWithdrawn(to, amount);
    }

    /// @notice Sweep accrued launch fees to the treasury. Callable by anyone; the
    ///         funds can only ever go to the configured treasury address.
    function withdrawTreasuryFees() external nonReentrant {
        uint256 amount = treasuryFees;
        require(amount != 0, "NO_FEES");
        treasuryFees = 0; // effect before interaction
        (bool ok,) = treasury.call{value: amount}("");
        require(ok, "ETH_TRANSFER_FAILED");
        emit TreasuryFeesWithdrawn(treasury, amount);
    }

    // ---------------------------------------------------------------------
    // Launch & trade
    // ---------------------------------------------------------------------

    /// @notice Launch a new token and open its bonding curve.
    /// @param  minTokensOut     Slippage guard for the optional first buy funded
    ///         by any ETH sent with this call.
    function createToken(string calldata name, string calldata symbol, uint256 minTokensOut, address referrer)
        external payable nonReentrant returns (address token)
    {
        token = _launchToken(name, symbol, minTokensOut, referrer, 0, 0);
    }

    /// @notice Launch with an immutable, short early-buy audience restriction.
    ///         Audience membership is evaluated by the authorized off-chain
    ///         signer; the contract guarantees signatures cannot be bypassed and
    ///         graduation cannot open permissionless DEX trading before expiry.
    function createTokenRestricted(
        string calldata name,
        string calldata symbol,
        uint256 minTokensOut,
        address referrer,
        uint32 restrictedFor,
        uint8 audiences
    ) external payable nonReentrant returns (address token) {
        token = _launchToken(name, symbol, minTokensOut, referrer, restrictedFor, audiences);
    }

    function _launchToken(
        string calldata name,
        string calldata symbol,
        uint256 minTokensOut,
        address referrer,
        uint32 restrictedFor,
        uint8 audiences
    ) internal returns (address token) {
        require(msg.value >= launchFee, "LAUNCH_FEE");
        token = _createToken(name, symbol, restrictedFor, audiences);
        // Flat launch fee to the treasury; whatever's left over funds the first buy.
        treasuryFees += launchFee;
        uint256 buyValue = msg.value - launchFee;
        // Optional same-tx first buy so the launcher can seed initial demand.
        if (buyValue != 0) {
            _setReferrer(msg.sender, referrer);
            _buy(token, msg.sender, msg.sender, msg.sender, buyValue, minTokensOut);
        }
        else require(minTokensOut == 0, "SLIPPAGE");
    }

    /// @notice Launch a token, optionally buy some for the creator, and atomically
    ///         buy a separate amount into a key-holder/community Merkle campaign.
    /// @dev Every campaign token is purchased through the same curve. No supply is
    ///      reserved or minted for the airdrop.
    function createTokenWithAirdrop(
        string calldata name,
        string calldata symbol,
        LaunchAirdropConfig calldata config
    ) external payable nonReentrant returns (address token, uint256 campaignId) {
        return _launchTokenWithAirdrop(name, symbol, config, 0, 0);
    }

    function createTokenWithAirdropRestricted(
        string calldata name,
        string calldata symbol,
        LaunchAirdropConfig calldata config,
        uint32 restrictedFor,
        uint8 audiences
    ) external payable nonReentrant returns (address token, uint256 campaignId) {
        return _launchTokenWithAirdrop(name, symbol, config, restrictedFor, audiences);
    }

    function _launchTokenWithAirdrop(
        string calldata name,
        string calldata symbol,
        LaunchAirdropConfig calldata config,
        uint32 restrictedFor,
        uint8 audiences
    ) internal returns (address token, uint256 campaignId) {
        require(config.airdropBuyEth != 0, "ZERO_AIRDROP_BUY");
        require(
            msg.value == launchFee + config.creatorBuyEth + config.airdropBuyEth,
            "BAD_MSG_VALUE"
        );

        token = _createToken(name, symbol, restrictedFor, audiences);
        treasuryFees += launchFee;
        _setReferrer(msg.sender, config.referrer);

        if (config.creatorBuyEth != 0) {
            _buy(
                token,
                msg.sender,
                msg.sender,
                msg.sender,
                config.creatorBuyEth,
                config.minCreatorTokensOut
            );
        } else require(config.minCreatorTokensOut == 0, "SLIPPAGE");

        (uint256 airdropTokens, uint256 ethSpent) = _buy(
            token,
            address(airdropVault),
            msg.sender,
            msg.sender,
            config.airdropBuyEth,
            config.minAirdropTokensOut
        );
        campaignId = airdropVault.registerLaunchCampaign(
            token,
            msg.sender,
            airdropTokens,
            config.merkleRoot,
            config.totalWeight,
            config.snapshotBlock,
            config.expiresAt
        );
        emit LaunchAirdropCreated(token, campaignId, ethSpent, airdropTokens);
    }

    function _createToken(
        string calldata name,
        string calldata symbol,
        uint32 restrictedFor,
        uint8 audiences
    ) internal returns (address token) {
        require(!creationPaused, "CREATION_UNAVAILABLE");
        require(address(migrator) != address(0) && address(airdropVault) != address(0), "NOT_CONFIGURED");
        bool openLaunch = restrictedFor == 0;
        require(
            openLaunch
                ? audiences == 0
                : (restrictedFor == 15 minutes || restrictedFor == 30 minutes || restrictedFor == 1 hours)
                    && audiences != 0 && audiences <= ALL_BUY_AUDIENCES,
            "BAD_RESTRICTION"
        );
        uint256 curveSupply = TOTAL_SUPPLY - LP_SUPPLY;
        uint128 scaledVirtualTokens;
        (token, scaledVirtualTokens) = _deployAndPrepare(name, symbol, curveSupply);
        curves[token] = Curve(
            virtualEthSeed,
            scaledVirtualTokens,
            0,
            uint128(curveSupply),
            msg.sender,
            false,
            false,
            false
        );
        uint48 restrictedUntil = openLaunch ? 0 : uint48(block.timestamp + restrictedFor);
        buyRestrictedUntil[token] = restrictedUntil;
        buyRestrictionAudiences[token] = audiences;
        allTokens.push(token);
        emit TokenCreated(token, msg.sender, name, symbol, curveSupply);
        emit BuyRestrictionSet(token, restrictedUntil, restrictedFor, audiences);
    }

    function _deployAndPrepare(string calldata name, string calldata symbol, uint256 curveSupply)
        internal returns (address token, uint128 scaledVirtualTokens)
    {
        bytes32 salt = keccak256(
            abi.encode(msg.sender, tokenNonce++, block.prevrandao, blockhash(block.number - 1), name, symbol)
        );
        token = address(
            new GreenTownToken{salt: salt}(
                name, symbol, TOTAL_SUPPLY, address(this), address(airdropVault)
            )
        );
        uint256 scaled = VIRTUAL_TOKEN_SEED * curveSupply / (TOTAL_SUPPLY - LP_SUPPLY);
        uint256 expectedRaisedEth = Math.ceilDiv(uint256(virtualEthSeed) * scaled, scaled - curveSupply) - virtualEthSeed;
        require(scaled <= type(uint128).max && expectedRaisedEth <= type(uint128).max, "PREPARATION_OVERFLOW");
        scaledVirtualTokens = uint128(scaled);
        graduationEthTarget[token] = uint128(expectedRaisedEth);
        (address poolManager, bytes32 poolId) = migrator.prepare(token, LP_SUPPLY, expectedRaisedEth);
        require(poolManager != address(0) && poolId != bytes32(0), "BAD_POOL");
        officialPool[token] = poolManager;
        officialPoolId[token] = poolId;
        emit PoolPrepared(token, poolManager, expectedRaisedEth);
        emit V4PoolPrepared(token, poolId, poolManager, expectedRaisedEth);
    }

    /// @notice Buy with an off-chain authorization signed by `authorizedSigner`.
    ///         Your backend decides eligibility (app-only, key-holder gating,
    ///         launch windows, ...) and signs a single-use `BuyAuth`. Works even
    ///         while enforcement is off (the signature check is then a no-op), so
    ///         the app can always route buys through this path.
    /// @param  maxEth   Most ETH this signature authorizes (msg.value <= maxEth).
    /// @param  deadline Unix time after which the signature is no longer valid.
    /// @param  nonce    Uniqueness value from your backend (prevents replay).
    function buySigned(
        address token,
        uint256 maxEth,
        uint256 deadline,
        uint256 nonce,
        bytes calldata signature,
        uint256 minTokensOut,
        address referrer
    ) external payable nonReentrant {
        require(msg.value != 0, "ZERO_AMOUNT");
        require(!buysPaused && !allTradingPaused && !curves[token].tradingPaused, "TRADING_PAUSED");
        _verifyBuyAuth(msg.sender, token, msg.value, maxEth, deadline, nonce, signature);
        _setReferrer(msg.sender, referrer);
        _buy(token, msg.sender, msg.sender, msg.sender, msg.value, minTokensOut);
    }

    /// @notice Sell tokens back to the curve for ETH.
    /// @dev    Checks-effects-interactions: reserves and fees are updated first,
    ///         then tokens are pulled in and ETH paid out. `nonReentrant` for belt
    ///         and braces. `ceilDiv` rounds the curve's favor so it can never be
    ///         drained by rounding dust.
    function sell(address token, uint256 amount, uint256 minEthOut) external nonReentrant {
        Curve storage c = curves[token];
        require(!allTradingPaused && !c.tradingPaused, "TRADING_PAUSED");
        require(c.citizen != address(0) && !c.graduated && amount != 0, "NOT_LIVE");
        uint256 ethOut = uint256(c.virtualEth) - Math.ceilDiv(uint256(c.virtualEth) * c.virtualTokens, uint256(c.virtualTokens) + amount);
        uint256 fee = ethOut * tradeFeeBps / BPS;
        uint256 payout = ethOut - fee;
        require(payout >= minEthOut && payout != 0, "SLIPPAGE");
        c.virtualEth -= uint128(ethOut);
        c.virtualTokens += uint128(amount);
        c.realEth -= uint128(ethOut);
        c.realTokens += uint128(amount);
        _accrueFee(token, msg.sender, fee);
        require(GreenTownToken(token).transferFrom(msg.sender, address(this), amount), "TRANSFER_FAILED");
        (bool ok,) = msg.sender.call{value: payout}("");
        require(ok, "ETH_TRANSFER_FAILED");
        emit Trade(token, msg.sender, false, payout, amount, fee);
    }

    /// @dev Shared buy engine. Handles the normal case, the final "sold-out" buy
    ///      (with an exact-fill + refund of any overshoot), fee accrual, and the
    ///      automatic graduation/migration once the curve empties.
    function _buy(
        address token,
        address recipient,
        address trader,
        address refundRecipient,
        uint256 ethIn,
        uint256 minTokensOut
    ) internal returns (uint256 tokensOut, uint256 grossSpent) {
        Curve storage c = curves[token];
        require(!buysPaused && !allTradingPaused && !c.tradingPaused, "TRADING_PAUSED");
        require(c.citizen != address(0) && !c.graduated, "NOT_LIVE");
        uint256 fee = ethIn * tradeFeeBps / BPS;
        uint256 curveEth = ethIn - fee;
        tokensOut = uint256(c.virtualTokens) - uint256(c.virtualEth) * c.virtualTokens / (uint256(c.virtualEth) + curveEth);
        uint256 refund;
        // Last buy: don't oversell. Solve for the exact ETH that finishes the
        // curve and refund whatever the buyer sent beyond that.
        if (tokensOut >= c.realTokens) {
            // Uniswap is permissionless. Keep at least the final curve purchase
            // unavailable until the creator's early-access window has ended, or
            // graduation would silently bypass the signed audience restriction.
            require(block.timestamp >= buyRestrictedUntil[token], "RESTRICTION_ACTIVE");
            tokensOut = c.realTokens;
            uint256 exactEth = Math.ceilDiv(uint256(c.virtualEth) * c.virtualTokens, uint256(c.virtualTokens) - tokensOut) - c.virtualEth;
            uint256 gross = Math.ceilDiv(exactEth * BPS, BPS - tradeFeeBps);
            if (gross > ethIn) gross = ethIn;
            curveEth = exactEth;
            fee = gross - exactEth;
            refund = ethIn - gross;
        }
        require(tokensOut >= minTokensOut && tokensOut != 0, "SLIPPAGE");
        c.virtualEth += uint128(curveEth);
        c.virtualTokens -= uint128(tokensOut);
        c.realEth += uint128(curveEth);
        c.realTokens -= uint128(tokensOut);
        _accrueFee(token, trader, fee);
        require(GreenTownToken(token).transfer(recipient, tokensOut), "TRANSFER_FAILED");
        grossSpent = curveEth + fee;
        emit Trade(token, trader, true, grossSpent, tokensOut, fee);

        // Graduation is all-or-nothing. The canonical v4 hook rejects every
        // pre-graduation swap, so external callers cannot move its prepared price.
        if (c.realTokens == 0) {
            _graduate(token);
        }
        // Return any overshoot from the final buy, last (after all state is settled).
        if (refund != 0) {
            (bool ok,) = refundRecipient.call{value: refund}("");
            require(ok, "REFUND_FAILED");
        }
    }

    function _graduate(address token) internal {
        Curve storage c = curves[token];
        c.graduated = true;
        uint256 liquidityEth = c.realEth;
        uint256 expectedEth = graduationEthTarget[token];
        uint256 targetDifference = liquidityEth > expectedEth ? liquidityEth - expectedEth : expectedEth - liquidityEth;
        require(targetDifference <= GRADUATION_ETH_TOLERANCE, "UNEXPECTED_GRADUATION_ETH");
        GreenTownToken(token).approve(address(migrator), LP_SUPPLY);
        uint256 balanceBeforeMigration = address(this).balance;
        (address pool, bytes32 poolId, uint256 tokenUsed, uint256 ethUsed) =
            migrator.migrate{value: liquidityEth}(token, LP_SUPPLY);
        require(
            pool == officialPool[token] && poolId == officialPoolId[token]
                && tokenUsed <= LP_SUPPLY && ethUsed <= liquidityEth
                && address(this).balance == balanceBeforeMigration - ethUsed,
            "BAD_MIGRATION"
        );
        GreenTownToken(token).approve(address(migrator), 0);
        c.migrated = true;
        c.realEth = 0;
        uint256 ethDust = liquidityEth - ethUsed;
        if (ethDust != 0) protocolFees += ethDust;
        emit Graduated(token, liquidityEth);
        emit Migrated(token, pool, ethUsed, tokenUsed);
        emit V4Migrated(token, poolId, pool, ethUsed, tokenUsed);
    }

    /// @notice A referrer withdraws the referral fees they've earned across tokens.
    /// @dev    Reentrancy-safe: balance zeroed before the transfer.
    function claimReferralFees() external nonReentrant {
        uint256 amount = referralFees[msg.sender];
        require(amount != 0, "NO_FEES");
        referralFees[msg.sender] = 0; // effect before interaction
        (bool ok,) = msg.sender.call{value: amount}("");
        require(ok, "ETH_TRANSFER_FAILED");
        emit ReferralFeesClaimed(msg.sender, amount);
    }

    /// @dev Record a trader's referrer — sticky (first referrer wins), never the
    ///      zero address and never self.
    function _setReferrer(address user, address referrer) internal {
        if (referrer != address(0) && referrer != user && userToReferrer[user] == address(0)) {
            userToReferrer[user] = referrer;
            emit ReferralSet(user, referrer);
        }
    }

    /// @dev Split a trade fee three ways: the token's citizen, the trader's
    ///      referrer (if any), and the protocol (the remainder). With no referrer,
    ///      the referral share simply stays with the protocol.
    function _accrueFee(address token, address trader, uint256 fee) internal {
        uint256 citizenCut = fee * citizenShareBps / BPS;
        uint256 referralCut = fee * referralShareBps / BPS;
        address referrer = userToReferrer[trader];
        if (referrer != address(0)) {
            referralFees[referrer] += referralCut;
        } else {
            referralCut = 0;
        }
        citizenFees[token] += citizenCut;
        protocolFees += fee - citizenCut - referralCut;
    }

    /// @dev EIP-712 domain separator — binds signatures to this contract and this
    ///      chain, so an authorization can't be replayed on another deployment.
    function _domainSeparator() internal view returns (bytes32) {
        return keccak256(
            abi.encode(
                EIP712_DOMAIN_TYPEHASH,
                keccak256(bytes("GreenTownLaunchpad")),
                keccak256(bytes("1")),
                block.chainid,
                address(this)
            )
        );
    }

    /// @dev Verify an off-chain buy authorization. A no-op while enforcement is
    ///      off (authorizedSigner == address(0)). Otherwise the signature must be
    ///      from the authorizer, unexpired, cover this ETH amount, and single-use.
    ///      We mark the digest used BEFORE the buy (effects before interactions),
    ///      and recover with OpenZeppelin's audited ECDSA (rejects malleable sigs).
    function _verifyBuyAuth(
        address buyer,
        address token,
        uint256 ethIn,
        uint256 maxEth,
        uint256 deadline,
        uint256 nonce,
        bytes calldata signature
    ) internal {
        address signer = authorizedSigner;
        if (signer == address(0)) {
            // Open launches retain the emergency enforcement-off behavior, but
            // a restricted launch must fail closed while no authorizer exists.
            require(block.timestamp >= buyRestrictedUntil[token], "AUTH_REQUIRED");
            return;
        }
        require(block.timestamp <= deadline, "AUTH_EXPIRED");
        require(ethIn <= maxEth, "AUTH_AMOUNT");
        bytes32 structHash = keccak256(abi.encode(BUY_AUTH_TYPEHASH, buyer, token, maxEth, deadline, nonce));
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _domainSeparator(), structHash));
        require(!usedBuyAuth[digest], "AUTH_USED");
        usedBuyAuth[digest] = true;
        require(digest.recover(signature) == signer, "BAD_AUTH");
    }

    /// @dev Accept only the tiny unused native-asset remainder returned by the
    ///      v4 migrator after locking the full-range position.
    receive() external payable { require(msg.sender == address(migrator), "ONLY_MIGRATOR"); }

    // ---------------------------------------------------------------------
    // Views
    // ---------------------------------------------------------------------

    /// @notice Preview a buy: how many tokens `ethIn` would return, and the fee.
    function quoteBuy(address token, uint256 ethIn) external view returns (uint256 tokensOut, uint256 fee) {
        Curve memory c = curves[token];
        fee = ethIn * tradeFeeBps / BPS;
        tokensOut = uint256(c.virtualTokens) - uint256(c.virtualEth) * c.virtualTokens / (uint256(c.virtualEth) + ethIn - fee);
        if (tokensOut > c.realTokens) tokensOut = c.realTokens;
    }

    /// @notice Number of tokens launched so far.
    function tokenCount() external view returns (uint256) { return allTokens.length; }

    /// @notice The citizen (launcher) of a given token.
    function citizenOf(address token) external view returns (address) { return curves[token].citizen; }
}
Chain explorer1778msChain node88ms