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
- Creator
- 0xD172b6aEa9…50F9f8017c
- 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