From 1735f1f805a504bdf279a2d7f32a0ec3fde6e0c4 Mon Sep 17 00:00:00 2001 From: LufyCZ Date: Sat, 11 Jul 2026 10:57:38 +0000 Subject: [PATCH 1/7] feat: add robinhood chain config --- config/robinhood.js | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 config/robinhood.js diff --git a/config/robinhood.js b/config/robinhood.js new file mode 100644 index 00000000..9eda33da --- /dev/null +++ b/config/robinhood.js @@ -0,0 +1,47 @@ +const NATIVE_ADDRESS = "0x0bd7d308f8e1639fab988df18a8011f41eacad73"; +const USDG_ADDRESS = "0x5fc5360d0400a0fd4f2af552add042d716f1d168"; + +module.exports = { + network: "robinhood-mainnet", + blocks: { + address: "0xe52abd50ad151ecdf56427effd715e703696a6b1", + startBlock: 0, + }, + v2: { + nativeAddress: NATIVE_ADDRESS, + whitelistAddresses: [ + // IMPORTANT! Native should be included here + NATIVE_ADDRESS, + USDG_ADDRESS, + ], + stable0: USDG_ADDRESS, + stable1: "0x0000000000000000000000000000000000000000", + stable2: "0x0000000000000000000000000000000000000000", + minimumNativeLiquidity: 0.5, + factory: { + address: "0xe52abd50ad151ecdf56427effd715e703696a6b1", + initCodeHash: + "0xe18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303", + startBlock: 6269958, + }, + }, + v3: { + factory: { + address: "0xe51960f1b45f1c9fb6d166e6a884f866fc70433b", + startBlock: 6292626, + }, + positionManager: { + address: "0x51d0e5188afe12d502e29d982d20c190e7816107", + startBlock: 6305802, + }, + native: { address: NATIVE_ADDRESS }, + whitelistedTokenAddresses: [ + // IMPORTANT! Native should be included here + NATIVE_ADDRESS, + USDG_ADDRESS, + ], + stableTokenAddresses: [USDG_ADDRESS], + nativePricePool: "0xc5a01c57b2851202dcf8507a0f2bd08a8025e2c8", // WETH/USDG - 0.3% (deterministic address) + minimumEthLocked: 0.5, + }, +}; From 4d565caa732ea4292dbb2dfc63bf38af7d55c821 Mon Sep 17 00:00:00 2001 From: LufyCZ Date: Wed, 22 Jul 2026 12:25:45 +0000 Subject: [PATCH 2/7] feat: initial launchpad subgraph --- config/robinhood.js | 11 + pnpm-lock.yaml | 18 + subgraphs/launchpad/README.md | 81 + subgraphs/launchpad/abis/SushiLaunchpad.json | 1340 +++++++++++++++++ subgraphs/launchpad/package.json | 23 + subgraphs/launchpad/schema.graphql | 213 +++ subgraphs/launchpad/src/mappings/helpers.ts | 98 ++ subgraphs/launchpad/src/mappings/launchpad.ts | 89 ++ subgraphs/launchpad/src/mappings/pool.ts | 95 ++ subgraphs/launchpad/src/mappings/token.ts | 217 +++ subgraphs/launchpad/template.yaml | 59 + subgraphs/launchpad/tests/.latest.json | 4 + subgraphs/launchpad/tests/Launchpad.test.ts | 395 +++++ subgraphs/launchpad/tests/mocks.ts | 453 ++++++ subgraphs/launchpad/tsconfig.json | 9 + 15 files changed, 3105 insertions(+) create mode 100644 subgraphs/launchpad/README.md create mode 100644 subgraphs/launchpad/abis/SushiLaunchpad.json create mode 100644 subgraphs/launchpad/package.json create mode 100644 subgraphs/launchpad/schema.graphql create mode 100644 subgraphs/launchpad/src/mappings/helpers.ts create mode 100644 subgraphs/launchpad/src/mappings/launchpad.ts create mode 100644 subgraphs/launchpad/src/mappings/pool.ts create mode 100644 subgraphs/launchpad/src/mappings/token.ts create mode 100644 subgraphs/launchpad/template.yaml create mode 100644 subgraphs/launchpad/tests/.latest.json create mode 100644 subgraphs/launchpad/tests/Launchpad.test.ts create mode 100644 subgraphs/launchpad/tests/mocks.ts create mode 100644 subgraphs/launchpad/tsconfig.json diff --git a/config/robinhood.js b/config/robinhood.js index 9eda33da..54f5da33 100644 --- a/config/robinhood.js +++ b/config/robinhood.js @@ -44,4 +44,15 @@ module.exports = { nativePricePool: "0xc5a01c57b2851202dcf8507a0f2bd08a8025e2c8", // WETH/USDG - 0.3% (deterministic address) minimumEthLocked: 0.5, }, + launchpad: { + deployments: [ + { + // Replace the pending address and block when SushiLaunchpad v1 is deployed. + name: "SushiLaunchpad", + chainId: 4663, + address: "0x0000000000000000000000000000000000000000", + startBlock: 0, + }, + ], + }, }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8b1d301..9727b649 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,24 @@ importers: specifier: 1.0.24 version: 1.0.24 + subgraphs/launchpad: + devDependencies: + '@graphprotocol/graph-cli': + specifier: ^0.96.0 + version: 0.96.0(typescript@5.8.2) + '@graphprotocol/graph-ts': + specifier: ^0.27.0 + version: 0.27.0 + assemblyscript: + specifier: ^0.19.20 + version: 0.19.23 + matchstick-as: + specifier: 0.6.0 + version: 0.6.0 + wabt: + specifier: 1.0.24 + version: 1.0.24 + subgraphs/route-processor: devDependencies: '@graphprotocol/graph-cli': diff --git a/subgraphs/launchpad/README.md b/subgraphs/launchpad/README.md new file mode 100644 index 00000000..16b6c5d9 --- /dev/null +++ b/subgraphs/launchpad/README.md @@ -0,0 +1,81 @@ +# Sushi Launchpad Subgraph + +Canonical, event-driven projection of Sushi Launchpad factories. Market swaps, +prices, candles, TVL, and volume stay in the existing Sushi V3 subgraph and the +data-api market projection. + +## Entities + +- `Launchpad` stores one factory deployment, its current protocol recipient, + native launch fee, default Sushi fee, protocol reserve settings, and + aggregate token, creator, and position counts. +- `Creator` provides the per-Launchpad identity needed to count distinct + creators and links each creator to their tokens. +- `Token` stores ERC-20 metadata, its creator, current Sushi fee, reserve state, + cumulative fee amounts, and its pool. +- `Pool` stores the immutable V3 configuration (`token0`, `token1`, fee, tick + spacing, and position manager) and links to its initial launch positions. +- `LaunchPosition` stores only the V3 NFTs minted in the token launch + transaction. Desired and used amounts are normalized to `amount0` and + `amount1` according to the pool's canonical token ordering. +- `FeeDistribution` stores each collection and split with both recipients and + all token0/token1 amounts. `ReserveWithdrawal` records the token's one + protocol-reserve withdrawal. +- `LaunchFeeWithdrawal` stores each withdrawal of accumulated native launch + fees with its amount and recipient. + +`Pool.positionCount` also reconciles the ordered `PositionCreated` events with +the final `TokenLaunched.positionCount`. No temporary accumulator entity is +needed: the pool and positions are permanent domain records even though the +contract emits `TokenLaunched` last. + +Every stable entity ID is chain-scoped even though each deployment indexes one +network. Addresses are stored as `Bytes`, whose GraphQL representation is +normalized lowercase hexadecimal. + +## Configuration + +Each `config/.js` file may register multiple current or historical +factory deployments: + +```js +launchpad: { + deployments: [ + { + name: "SushiLaunchpadV1", + chainId: 4663, + address: "0x...", + startBlock: 123, + }, + ], +} +``` + +All deployments on a network share the schema and mappings. The mapping reads +the quote token, position manager, protocol recipient, launch fee, and initial +defaults from the Launchpad contract, then reads canonical token ordering, fee, +and tick spacing from each created V3 pool. Add old and new factory versions to +the array during a rotation; do not replace historical entries. Keep the first +historical data source named `SushiLaunchpad`; the shared mapping imports its +generated ABI bindings, while later entries use unique names such as +`SushiLaunchpadV2`. + +The mapping is split by responsibility: `helpers.ts` owns chain-scoped IDs and +entity loading, `token.ts` owns token/pool/position and token-level flows, and +`launchpad.ts` owns factory-wide settings and provides the manifest exports. + +Robinhood currently contains a zero-address build placeholder because the +production factory address and deployment block are still an explicit launch +gate in the contract specification. Replace both values before deployment. + +## Development + +```sh +NETWORK=robinhood pnpm generate +pnpm build +pnpm test +``` + +`abis/SushiLaunchpad.json` is pinned from the current contract artifact in the +`sushi-launchpad` repository. Refresh it together with mappings and tests when +an indexed event changes. diff --git a/subgraphs/launchpad/abis/SushiLaunchpad.json b/subgraphs/launchpad/abis/SushiLaunchpad.json new file mode 100644 index 00000000..950cf13c --- /dev/null +++ b/subgraphs/launchpad/abis/SushiLaunchpad.json @@ -0,0 +1,1340 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "initialOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "initialProtocolRecipient", + "type": "address" + }, + { + "internalType": "address", + "name": "factory_", + "type": "address" + }, + { + "internalType": "address", + "name": "positionManager_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "deadline", + "type": "uint64" + } + ], + "name": "DeadlineExpired", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyName", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyRanges", + "type": "error" + }, + { + "inputs": [], + "name": "EmptySymbol", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "used", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "desired", + "type": "uint256" + } + ], + "name": "ExcessTokenConsumption", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "required", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "supplied", + "type": "uint256" + } + ], + "name": "InsufficientLaunchFee", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "bps", + "type": "uint16" + } + ], + "name": "InvalidFeeBps", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPositionManager", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "startTick", + "type": "int24" + }, + { + "internalType": "int24", + "name": "endTick", + "type": "int24" + } + ], + "name": "InvalidRangeOrder", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "bps", + "type": "uint16" + } + ], + "name": "InvalidReserveBps", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "InvalidTickBounds", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "actualTickSpacing", + "type": "int24" + } + ], + "name": "InvalidTickSpacing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "InvalidTickSpacingAt", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "NameTooLong", + "type": "error" + }, + { + "inputs": [], + "name": "NativeTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "NoCleanTokenAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "int24", + "name": "previousEndTick", + "type": "int24" + }, + { + "internalType": "int24", + "name": "startTick", + "type": "int24" + } + ], + "name": "NonContiguousRanges", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "NonzeroWethUsed", + "type": "error" + }, + { + "inputs": [], + "name": "NothingToWithdraw", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "OwnershipRenunciationDisabled", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolConfigurationMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "expected", + "type": "address" + }, + { + "internalType": "address", + "name": "actual", + "type": "address" + } + ], + "name": "PoolCreationMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint160", + "name": "expected", + "type": "uint160" + }, + { + "internalType": "uint160", + "name": "actual", + "type": "uint160" + } + ], + "name": "PoolInitializationMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + } + ], + "name": "PositionConfigurationMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "RangeAmountOverflow", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "supplied", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "allocation", + "type": "uint256" + } + ], + "name": "RangeAmountsExceedAllocation", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "ReserveAlreadyWithdrawn", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "unlockAt", + "type": "uint64" + } + ], + "name": "ReserveStillLocked", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "accounted", + "type": "uint256" + } + ], + "name": "SupplyReconciliationFailure", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "SymbolTooLong", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "supplied", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maximum", + "type": "uint256" + } + ], + "name": "TooManyRanges", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "UnexpectedExistingPool", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collection", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + } + ], + "name": "UnexpectedNft", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "UnknownToken", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "ZeroLiquidity", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "ZeroRangeAmount", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "previousBps", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "newBps", + "type": "uint16" + } + ], + "name": "DefaultSushiFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "protocolRecipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "sushiFeeBps", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wethCollected", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenCollected", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wethToSushi", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenToSushi", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "wethToCreator", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenToCreator", + "type": "uint256" + } + ], + "name": "FeesDistributed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "previousFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "LaunchFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "LaunchFeesWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferStarted", + "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": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenDesired", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokenUsed", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + } + ], + "name": "PositionCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousRecipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newRecipient", + "type": "address" + } + ], + "name": "ProtocolRecipientUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "previousBps", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "newBps", + "type": "uint16" + } + ], + "name": "ProtocolReserveBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ProtocolReserveWithdrawn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "previousSushiFeeBps", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "newSushiFeeBps", + "type": "uint16" + } + ], + "name": "SushiFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "totalSupply", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "reserveBps", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "reserveAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "reserveUnlockAt", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "initialSushiFeeBps", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "positionCount", + "type": "uint256" + } + ], + "name": "TokenLaunched", + "type": "event" + }, + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "acceptOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "defaultSushiFeeBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "distributeFees", + "outputs": [ + { + "internalType": "uint256", + "name": "wethCollected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenCollected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "wethToSushi", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenToSushi", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "internalType": "struct ISushiLaunchpad.TokenConfig", + "name": "tokenConfig", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "int24", + "name": "startTick", + "type": "int24" + }, + { + "internalType": "int24", + "name": "endTick", + "type": "int24" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ISushiLaunchpad.SaleRange[]", + "name": "ranges", + "type": "tuple[]" + }, + { + "internalType": "uint64", + "name": "deadline", + "type": "uint64" + } + ], + "name": "launch", + "outputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "positionIds", + "type": "uint256[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "launchFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "launchInfo", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint64", + "name": "reserveUnlockAt", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "reserveWithdrawn", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "reserveAmount", + "type": "uint256" + } + ], + "internalType": "struct ISushiLaunchpad.LaunchInfo", + "name": "info", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onERC721Received", + "outputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "positionManager", + "outputs": [ + { + "internalType": "contract INonfungiblePositionManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolRecipient", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolReserveBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "newSushiFeeBps", + "type": "uint16" + } + ], + "name": "setDefaultSushiFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newLaunchFee", + "type": "uint256" + } + ], + "name": "setLaunchFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newRecipient", + "type": "address" + } + ], + "name": "setProtocolRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "newReserveBps", + "type": "uint16" + } + ], + "name": "setProtocolReserveBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint16", + "name": "newSushiFeeBps", + "type": "uint16" + } + ], + "name": "setSushiFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "sushiFeeBps", + "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": "v3Factory", + "outputs": [ + { + "internalType": "contract ISushiV3Factory", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawLaunchFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "withdrawProtocolReserve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/subgraphs/launchpad/package.json b/subgraphs/launchpad/package.json new file mode 100644 index 00000000..20315766 --- /dev/null +++ b/subgraphs/launchpad/package.json @@ -0,0 +1,23 @@ +{ + "name": "launchpad", + "license": "MIT", + "repository": { + "url": "sushiswap/subgraphs", + "directory": "subgraphs/launchpad" + }, + "files": [ + "generated" + ], + "scripts": { + "generate": "mustache ../../config/$NETWORK.js template.yaml > subgraph.yaml && graph codegen", + "build": "graph build", + "test": "graph test -r" + }, + "devDependencies": { + "@graphprotocol/graph-cli": "^0.96.0", + "@graphprotocol/graph-ts": "^0.27.0", + "assemblyscript": "^0.19.20", + "matchstick-as": "0.6.0", + "wabt": "1.0.24" + } +} diff --git a/subgraphs/launchpad/schema.graphql b/subgraphs/launchpad/schema.graphql new file mode 100644 index 00000000..f2f7d0de --- /dev/null +++ b/subgraphs/launchpad/schema.graphql @@ -0,0 +1,213 @@ +type Launchpad @entity { + # Identity + id: ID! + chainId: BigInt! + address: Bytes! + + # Deployment configuration + quoteToken: Bytes! + positionManager: Bytes! + + # Current protocol configuration + protocolRecipient: Bytes! + launchFee: BigInt! + defaultSushiFeeBps: Int! + protocolReserveBps: Int! + + # Aggregate metrics + tokenCount: Int! + creatorCount: Int! + positionCount: Int! + + # Relationships + tokens: [Token!]! @derivedFrom(field: "launchpad") + pools: [Pool!]! @derivedFrom(field: "launchpad") + creators: [Creator!]! @derivedFrom(field: "launchpad") + launchFeeWithdrawals: [LaunchFeeWithdrawal!]! @derivedFrom(field: "launchpad") +} + +type Creator @entity { + # Identity + id: ID! + chainId: BigInt! + address: Bytes! + + # Relationships + launchpad: Launchpad! + tokens: [Token!]! @derivedFrom(field: "creator") + + # Aggregate metrics + tokenCount: Int! +} + +type Token @entity { + # Identity + id: ID! + chainId: BigInt! + address: Bytes! + + # Relationships + launchpad: Launchpad! + creator: Creator! + pool: Pool! + + # ERC-20 metadata + name: String! + symbol: String! + decimals: Int! + totalSupply: BigInt! + + # Current fee configuration + sushiFeeBps: Int! + + # Reserve state + reserveBps: Int! + reserveAmount: BigInt! + reserveUnlockAt: BigInt! + reserveWithdrawn: Boolean! + reserveWithdrawal: ReserveWithdrawal + + # Fee distribution metrics + totalAmount0Collected: BigInt! + totalAmount1Collected: BigInt! + totalAmount0ToSushi: BigInt! + totalAmount1ToSushi: BigInt! + totalAmount0ToCreator: BigInt! + totalAmount1ToCreator: BigInt! + feeDistributions: [FeeDistribution!]! @derivedFrom(field: "token") + + # Creation metadata + creationTransactionHash: Bytes! + creationLogIndex: BigInt! + creationBlockNumber: BigInt! + creationBlockHash: Bytes! + createdAt: BigInt! +} + +type Pool @entity { + # Identity + id: ID! + chainId: BigInt! + address: Bytes! + + # Relationships + launchpad: Launchpad! + positions: [LaunchPosition!]! @derivedFrom(field: "pool") + + # Immutable pool configuration + token0: Bytes! + token1: Bytes! + fee: Int! + tickSpacing: Int! + positionManager: Bytes! + + # Aggregate metrics + positionCount: Int! + + # Creation metadata + creationTransactionHash: Bytes! + creationBlockNumber: BigInt! + creationBlockHash: Bytes! + createdAt: BigInt! +} + +type LaunchPosition @entity(immutable: true) { + # Identity + id: ID! + chainId: BigInt! + positionManager: Bytes! + positionId: BigInt! + index: Int! + + # Relationships + pool: Pool! + + # Position configuration + tickLower: Int! + tickUpper: Int! + liquidity: BigInt! + + # Initial amounts + amount0Desired: BigInt! + amount1Desired: BigInt! + amount0: BigInt! + amount1: BigInt! + + # Creation metadata + creationTransactionHash: Bytes! + creationLogIndex: BigInt! + creationBlockNumber: BigInt! + creationBlockHash: Bytes! + createdAt: BigInt! +} + +type FeeDistribution @entity(immutable: true) { + # Identity + id: ID! + chainId: BigInt! + + # Relationships + token: Token! + pool: Pool! + + # Participants and fee configuration + caller: Bytes! + sushiRecipient: Bytes! + creatorRecipient: Bytes! + sushiFeeBps: Int! + + # Distributed amounts + amount0Collected: BigInt! + amount1Collected: BigInt! + amount0ToSushi: BigInt! + amount1ToSushi: BigInt! + amount0ToCreator: BigInt! + amount1ToCreator: BigInt! + + # Event metadata + transactionHash: Bytes! + logIndex: BigInt! + blockNumber: BigInt! + blockHash: Bytes! + timestamp: BigInt! +} + +type ReserveWithdrawal @entity(immutable: true) { + # Identity + id: ID! + chainId: BigInt! + + # Relationships + token: Token! + + # Withdrawal + recipient: Bytes! + amount: BigInt! + + # Event metadata + transactionHash: Bytes! + logIndex: BigInt! + blockNumber: BigInt! + blockHash: Bytes! + timestamp: BigInt! +} + +type LaunchFeeWithdrawal @entity(immutable: true) { + # Identity + id: ID! + chainId: BigInt! + + # Relationships + launchpad: Launchpad! + + # Withdrawal + recipient: Bytes! + amount: BigInt! + + # Event metadata + transactionHash: Bytes! + logIndex: BigInt! + blockNumber: BigInt! + blockHash: Bytes! + timestamp: BigInt! +} diff --git a/subgraphs/launchpad/src/mappings/helpers.ts b/subgraphs/launchpad/src/mappings/helpers.ts new file mode 100644 index 00000000..d765dd79 --- /dev/null +++ b/subgraphs/launchpad/src/mappings/helpers.ts @@ -0,0 +1,98 @@ +import { + Address, + BigInt, + Bytes, + dataSource, + ethereum, +} from "@graphprotocol/graph-ts"; +import { SushiLaunchpad as SushiLaunchpadContract } from "../../generated/SushiLaunchpad/SushiLaunchpad"; +import { Launchpad, Pool, Token } from "../../generated/schema"; + +export class DeploymentContext { + chainId: BigInt; + + constructor(chainId: BigInt) { + this.chainId = chainId; + } +} + +export function deploymentContext(): DeploymentContext { + const context = dataSource.context(); + assert( + context.get("chainId") != null, + "Launchpad data source context is incomplete" + ); + + return new DeploymentContext(context.getBigInt("chainId")); +} + +export function addressId(chainId: BigInt, address: Bytes): string { + return chainId.toString().concat(":").concat(address.toHexString()); +} + +export function creatorId(launchpad: Launchpad, address: Bytes): string { + return launchpad.id.concat(":").concat(address.toHexString()); +} + +export function positionId( + chainId: BigInt, + positionManager: Bytes, + onchainPositionId: BigInt +): string { + return addressId(chainId, positionManager) + .concat(":") + .concat(onchainPositionId.toString()); +} + +export function eventId(chainId: BigInt, event: ethereum.Event): string { + return chainId + .toString() + .concat(":") + .concat(event.transaction.hash.toHexString()) + .concat(":") + .concat(event.logIndex.toString()); +} + +export function getOrCreateLaunchpad( + context: DeploymentContext, + factory: Address +): Launchpad { + const id = addressId(context.chainId, factory); + let launchpad = Launchpad.load(id); + if (launchpad == null) { + // Contract calls establish the first block-visible state. Mutable values + // are subsequently owned exclusively by their dedicated update handlers. + const contract = SushiLaunchpadContract.bind(factory); + launchpad = new Launchpad(id); + launchpad.chainId = context.chainId; + launchpad.address = factory; + launchpad.quoteToken = contract.WETH(); + launchpad.positionManager = contract.positionManager(); + launchpad.protocolRecipient = contract.protocolRecipient(); + launchpad.launchFee = contract.launchFee(); + launchpad.defaultSushiFeeBps = contract.defaultSushiFeeBps(); + launchpad.protocolReserveBps = contract.protocolReserveBps(); + launchpad.tokenCount = 0; + launchpad.creatorCount = 0; + launchpad.positionCount = 0; + launchpad.save(); + } + return changetype(launchpad); +} + +export function requireToken( + context: DeploymentContext, + address: Bytes +): Token { + const id = addressId(context.chainId, address); + const token = Token.load(id); + assert(token != null, "Launchpad event references unknown token " + id); + return changetype(token); +} + +export function requirePool(context: DeploymentContext, address: Bytes): Pool { + const id = addressId(context.chainId, address); + const pool = Pool.load(id); + assert(pool != null, "Launchpad event references unknown pool " + id); + return changetype(pool); +} diff --git a/subgraphs/launchpad/src/mappings/launchpad.ts b/subgraphs/launchpad/src/mappings/launchpad.ts new file mode 100644 index 00000000..acdbabcd --- /dev/null +++ b/subgraphs/launchpad/src/mappings/launchpad.ts @@ -0,0 +1,89 @@ +import { + DefaultSushiFeeBpsUpdated as DefaultSushiFeeBpsUpdatedEvent, + LaunchFeesWithdrawn as LaunchFeesWithdrawnEvent, + LaunchFeeUpdated as LaunchFeeUpdatedEvent, + ProtocolRecipientUpdated as ProtocolRecipientUpdatedEvent, + ProtocolReserveBpsUpdated as ProtocolReserveBpsUpdatedEvent, +} from "../../generated/SushiLaunchpad/SushiLaunchpad"; +import { LaunchFeeWithdrawal } from "../../generated/schema"; +import { deploymentContext, eventId, getOrCreateLaunchpad } from "./helpers"; + +const BPS_DENOMINATOR = 10_000; +const MAX_PROTOCOL_RESERVE_BPS = 1_000; + +export function handleDefaultSushiFeeBpsUpdated( + event: DefaultSushiFeeBpsUpdatedEvent +): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + assert( + event.params.newBps <= BPS_DENOMINATOR, + "Invalid default Sushi fee update for " + launchpad.id + ); + + launchpad.defaultSushiFeeBps = event.params.newBps; + launchpad.save(); +} + +export function handleProtocolReserveBpsUpdated( + event: ProtocolReserveBpsUpdatedEvent +): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + assert( + event.params.newBps <= MAX_PROTOCOL_RESERVE_BPS, + "Invalid protocol reserve update for " + launchpad.id + ); + + launchpad.protocolReserveBps = event.params.newBps; + launchpad.save(); +} + +export function handleProtocolRecipientUpdated( + event: ProtocolRecipientUpdatedEvent +): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + launchpad.protocolRecipient = event.params.newRecipient; + launchpad.save(); +} + +export function handleLaunchFeeUpdated(event: LaunchFeeUpdatedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + launchpad.launchFee = event.params.newFee; + launchpad.save(); +} + +export function handleLaunchFeesWithdrawn( + event: LaunchFeesWithdrawnEvent +): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const id = eventId(context.chainId, event); + assert( + LaunchFeeWithdrawal.load(id) == null, + "Duplicate launch fee withdrawal " + id + ); + + const withdrawal = new LaunchFeeWithdrawal(id); + withdrawal.chainId = context.chainId; + withdrawal.launchpad = launchpad.id; + withdrawal.recipient = event.params.recipient; + withdrawal.amount = event.params.amount; + withdrawal.transactionHash = event.transaction.hash; + withdrawal.logIndex = event.logIndex; + withdrawal.blockNumber = event.block.number; + withdrawal.blockHash = event.block.hash; + withdrawal.timestamp = event.block.timestamp; + withdrawal.save(); +} + +export { + handleFeesDistributed, + handleProtocolReserveWithdrawn, + handleSushiFeeBpsUpdated, + handleTokenLaunched, +} from "./token"; + +export { handlePositionCreated } from "./pool"; diff --git a/subgraphs/launchpad/src/mappings/pool.ts b/subgraphs/launchpad/src/mappings/pool.ts new file mode 100644 index 00000000..3227e7b3 --- /dev/null +++ b/subgraphs/launchpad/src/mappings/pool.ts @@ -0,0 +1,95 @@ +import { BigInt } from "@graphprotocol/graph-ts"; +import { PositionCreated as PositionCreatedEvent } from "../../generated/SushiLaunchpad/SushiLaunchpad"; +import { SushiV3Pool } from "../../generated/SushiLaunchpad/SushiV3Pool"; +import { LaunchPosition, Pool, Token } from "../../generated/schema"; +import { + addressId, + deploymentContext, + getOrCreateLaunchpad, + positionId, +} from "./helpers"; + +export function handlePositionCreated(event: PositionCreatedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const idForToken = addressId(context.chainId, event.params.token); + assert( + Token.load(idForToken) == null, + "Position emitted after token launch " + idForToken + ); + + const idForPool = addressId(context.chainId, event.params.pool); + let pool = Pool.load(idForPool); + if (pool == null) { + const contract = SushiV3Pool.bind(event.params.pool); + const token0 = contract.token0(); + const token1 = contract.token1(); + assert( + (token0.equals(event.params.token) && + token1.equals(launchpad.quoteToken)) || + (token0.equals(launchpad.quoteToken) && + token1.equals(event.params.token)), + "Launch position references unexpected pool pair " + idForPool + ); + pool = new Pool(idForPool); + pool.chainId = context.chainId; + pool.launchpad = launchpad.id; + pool.address = event.params.pool; + pool.token0 = token0; + pool.token1 = token1; + pool.fee = contract.fee(); + pool.tickSpacing = contract.tickSpacing(); + pool.positionManager = launchpad.positionManager; + pool.positionCount = 0; + pool.creationTransactionHash = event.transaction.hash; + pool.creationBlockNumber = event.block.number; + pool.creationBlockHash = event.block.hash; + pool.createdAt = event.block.timestamp; + } else { + assert( + pool.launchpad == launchpad.id && + pool.address.equals(event.params.pool) && + pool.positionManager.equals(launchpad.positionManager) && + (pool.token0.equals(event.params.token) || + pool.token1.equals(event.params.token)), + "Inconsistent ordered position events for pool " + idForPool + ); + } + + const idForPosition = positionId( + context.chainId, + launchpad.positionManager, + event.params.positionId + ); + assert( + LaunchPosition.load(idForPosition) == null, + "Duplicate launch position " + idForPosition + ); + + const zero = BigInt.zero(); + const tokenIs0 = pool.token0.equals(event.params.token); + const position = new LaunchPosition(idForPosition); + position.chainId = context.chainId; + position.pool = pool.id; + position.positionManager = launchpad.positionManager; + position.positionId = event.params.positionId; + position.index = pool.positionCount; + position.tickLower = event.params.tickLower; + position.tickUpper = event.params.tickUpper; + position.amount0Desired = tokenIs0 ? event.params.tokenDesired : zero; + position.amount1Desired = tokenIs0 ? zero : event.params.tokenDesired; + position.amount0 = tokenIs0 ? event.params.tokenUsed : zero; + position.amount1 = tokenIs0 ? zero : event.params.tokenUsed; + position.liquidity = event.params.liquidity; + position.creationTransactionHash = event.transaction.hash; + position.creationLogIndex = event.logIndex; + position.creationBlockNumber = event.block.number; + position.creationBlockHash = event.block.hash; + position.createdAt = event.block.timestamp; + position.save(); + + pool.positionCount += 1; + pool.save(); + launchpad.positionCount += 1; + launchpad.save(); +} diff --git a/subgraphs/launchpad/src/mappings/token.ts b/subgraphs/launchpad/src/mappings/token.ts new file mode 100644 index 00000000..4a41c419 --- /dev/null +++ b/subgraphs/launchpad/src/mappings/token.ts @@ -0,0 +1,217 @@ +import { BigInt } from "@graphprotocol/graph-ts"; +import { + FeesDistributed as FeesDistributedEvent, + ProtocolReserveWithdrawn as ProtocolReserveWithdrawnEvent, + SushiFeeBpsUpdated as SushiFeeBpsUpdatedEvent, + TokenLaunched as TokenLaunchedEvent, +} from "../../generated/SushiLaunchpad/SushiLaunchpad"; +import { + Creator, + FeeDistribution, + ReserveWithdrawal, + Token, +} from "../../generated/schema"; +import { + addressId, + creatorId, + deploymentContext, + eventId, + getOrCreateLaunchpad, + requirePool, + requireToken, +} from "./helpers"; + +const BPS_DENOMINATOR = 10_000; + +export function handleTokenLaunched(event: TokenLaunchedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const idForToken = addressId(context.chainId, event.params.token); + assert( + Token.load(idForToken) == null, + "Duplicate token launch " + idForToken + ); + + const pool = requirePool(context, event.params.pool); + assert( + pool.launchpad == launchpad.id && + (pool.token0.equals(event.params.token) || + pool.token1.equals(event.params.token)) && + event.params.positionCount.equals(BigInt.fromI32(pool.positionCount)), + "Token launch does not reconcile with pool " + pool.id + ); + + const idForCreator = creatorId(launchpad, event.params.creator); + let creator = Creator.load(idForCreator); + if (creator == null) { + creator = new Creator(idForCreator); + creator.chainId = context.chainId; + creator.launchpad = launchpad.id; + creator.address = event.params.creator; + creator.tokenCount = 0; + launchpad.creatorCount += 1; + } + creator.tokenCount += 1; + creator.save(); + + const zero = BigInt.zero(); + const token = new Token(idForToken); + token.chainId = context.chainId; + token.launchpad = launchpad.id; + token.address = event.params.token; + token.creator = creator.id; + token.pool = pool.id; + token.name = event.params.name; + token.symbol = event.params.symbol; + token.decimals = event.params.decimals; + token.totalSupply = event.params.totalSupply; + token.sushiFeeBps = event.params.initialSushiFeeBps; + token.reserveBps = event.params.reserveBps; + token.reserveAmount = event.params.reserveAmount; + token.reserveUnlockAt = event.params.reserveUnlockAt; + token.reserveWithdrawn = false; + token.totalAmount0Collected = zero; + token.totalAmount1Collected = zero; + token.totalAmount0ToSushi = zero; + token.totalAmount1ToSushi = zero; + token.totalAmount0ToCreator = zero; + token.totalAmount1ToCreator = zero; + token.creationTransactionHash = event.transaction.hash; + token.creationLogIndex = event.logIndex; + token.creationBlockNumber = event.block.number; + token.creationBlockHash = event.block.hash; + token.createdAt = event.block.timestamp; + token.save(); + + launchpad.tokenCount += 1; + launchpad.save(); +} + +export function handleSushiFeeBpsUpdated(event: SushiFeeBpsUpdatedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const token = requireToken(context, event.params.token); + assert( + token.launchpad == launchpad.id && + token.sushiFeeBps == event.params.previousSushiFeeBps && + event.params.newSushiFeeBps <= BPS_DENOMINATOR, + "Sushi fee update does not reconcile for " + token.id + ); + + token.sushiFeeBps = event.params.newSushiFeeBps; + token.save(); +} + +export function handleFeesDistributed(event: FeesDistributedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const token = requireToken(context, event.params.token); + const pool = requirePool(context, event.params.pool); + const creator = Creator.load(token.creator); + assert(creator != null, "Token references unknown creator " + token.creator); + const tokenCreator = changetype(creator); + assert( + token.launchpad == launchpad.id && + token.pool == pool.id && + tokenCreator.address.equals(event.params.creator) && + token.sushiFeeBps == event.params.sushiFeeBps && + event.params.wethCollected.equals( + event.params.wethToSushi.plus(event.params.wethToCreator) + ) && + event.params.tokenCollected.equals( + event.params.tokenToSushi.plus(event.params.tokenToCreator) + ), + "Fee distribution does not reconcile for " + token.id + ); + + const id = eventId(context.chainId, event); + assert(FeeDistribution.load(id) == null, "Duplicate fee distribution " + id); + const tokenIs0 = pool.token0.equals(event.params.token); + const distribution = new FeeDistribution(id); + distribution.chainId = context.chainId; + distribution.token = token.id; + distribution.pool = pool.id; + distribution.caller = event.params.caller; + distribution.sushiRecipient = event.params.protocolRecipient; + distribution.creatorRecipient = event.params.creator; + distribution.sushiFeeBps = event.params.sushiFeeBps; + distribution.amount0Collected = tokenIs0 + ? event.params.tokenCollected + : event.params.wethCollected; + distribution.amount1Collected = tokenIs0 + ? event.params.wethCollected + : event.params.tokenCollected; + distribution.amount0ToSushi = tokenIs0 + ? event.params.tokenToSushi + : event.params.wethToSushi; + distribution.amount1ToSushi = tokenIs0 + ? event.params.wethToSushi + : event.params.tokenToSushi; + distribution.amount0ToCreator = tokenIs0 + ? event.params.tokenToCreator + : event.params.wethToCreator; + distribution.amount1ToCreator = tokenIs0 + ? event.params.wethToCreator + : event.params.tokenToCreator; + distribution.transactionHash = event.transaction.hash; + distribution.logIndex = event.logIndex; + distribution.blockNumber = event.block.number; + distribution.blockHash = event.block.hash; + distribution.timestamp = event.block.timestamp; + distribution.save(); + + token.totalAmount0Collected = token.totalAmount0Collected.plus( + distribution.amount0Collected + ); + token.totalAmount1Collected = token.totalAmount1Collected.plus( + distribution.amount1Collected + ); + token.totalAmount0ToSushi = token.totalAmount0ToSushi.plus( + distribution.amount0ToSushi + ); + token.totalAmount1ToSushi = token.totalAmount1ToSushi.plus( + distribution.amount1ToSushi + ); + token.totalAmount0ToCreator = token.totalAmount0ToCreator.plus( + distribution.amount0ToCreator + ); + token.totalAmount1ToCreator = token.totalAmount1ToCreator.plus( + distribution.amount1ToCreator + ); + token.save(); +} + +export function handleProtocolReserveWithdrawn( + event: ProtocolReserveWithdrawnEvent +): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const token = requireToken(context, event.params.token); + assert( + token.launchpad == launchpad.id && + !token.reserveWithdrawn && + event.params.amount.equals(token.reserveAmount), + "Reserve withdrawal does not reconcile for " + token.id + ); + + const id = eventId(context.chainId, event); + assert( + ReserveWithdrawal.load(id) == null, + "Duplicate reserve withdrawal " + id + ); + const withdrawal = new ReserveWithdrawal(id); + withdrawal.chainId = context.chainId; + withdrawal.token = token.id; + withdrawal.recipient = event.params.recipient; + withdrawal.amount = event.params.amount; + withdrawal.transactionHash = event.transaction.hash; + withdrawal.logIndex = event.logIndex; + withdrawal.blockNumber = event.block.number; + withdrawal.blockHash = event.block.hash; + withdrawal.timestamp = event.block.timestamp; + withdrawal.save(); + + token.reserveWithdrawn = true; + token.reserveWithdrawal = withdrawal.id; + token.save(); +} diff --git a/subgraphs/launchpad/template.yaml b/subgraphs/launchpad/template.yaml new file mode 100644 index 00000000..0cbe7fec --- /dev/null +++ b/subgraphs/launchpad/template.yaml @@ -0,0 +1,59 @@ +specVersion: 1.0.0 +description: Canonical Sushi Launchpad registry and management activity +repository: https://github.com/sushiswap/subgraphs +schema: + file: ./schema.graphql +dataSources: + {{#launchpad.deployments}} + - kind: ethereum/contract + name: {{ name }} + network: {{ network }} + context: + chainId: + type: BigInt + data: '{{ chainId }}' + source: + address: '{{ address }}' + startBlock: {{ startBlock }} + abi: SushiLaunchpad + mapping: + kind: ethereum/events + apiVersion: 0.0.9 + language: wasm/assemblyscript + file: ./src/mappings/launchpad.ts + entities: + - Launchpad + - Creator + - Token + - Pool + - LaunchPosition + - FeeDistribution + - ReserveWithdrawal + - LaunchFeeWithdrawal + abis: + - name: SushiLaunchpad + file: ./abis/SushiLaunchpad.json + - name: SushiV3Pool + file: ../v3/abis/pool.json + eventHandlers: + - event: TokenLaunched(indexed address,indexed address,indexed address,string,string,uint8,uint256,uint16,uint256,uint64,uint16,uint256) + handler: handleTokenLaunched + - event: PositionCreated(indexed address,indexed address,indexed uint256,int24,int24,uint256,uint256,uint128) + handler: handlePositionCreated + - event: SushiFeeBpsUpdated(indexed address,uint16,uint16) + handler: handleSushiFeeBpsUpdated + - event: DefaultSushiFeeBpsUpdated(uint16,uint16) + handler: handleDefaultSushiFeeBpsUpdated + - event: ProtocolReserveBpsUpdated(uint16,uint16) + handler: handleProtocolReserveBpsUpdated + - event: ProtocolRecipientUpdated(indexed address,indexed address) + handler: handleProtocolRecipientUpdated + - event: LaunchFeeUpdated(uint256,uint256) + handler: handleLaunchFeeUpdated + - event: LaunchFeesWithdrawn(indexed address,uint256) + handler: handleLaunchFeesWithdrawn + - event: FeesDistributed(indexed address,indexed address,indexed address,address,address,uint16,uint256,uint256,uint256,uint256,uint256,uint256) + handler: handleFeesDistributed + - event: ProtocolReserveWithdrawn(indexed address,indexed address,uint256) + handler: handleProtocolReserveWithdrawn + {{/launchpad.deployments}} diff --git a/subgraphs/launchpad/tests/.latest.json b/subgraphs/launchpad/tests/.latest.json new file mode 100644 index 00000000..788b76b8 --- /dev/null +++ b/subgraphs/launchpad/tests/.latest.json @@ -0,0 +1,4 @@ +{ + "version": "0.6.0", + "timestamp": 1784652490157 +} \ No newline at end of file diff --git a/subgraphs/launchpad/tests/Launchpad.test.ts b/subgraphs/launchpad/tests/Launchpad.test.ts new file mode 100644 index 00000000..e3fe9a3f --- /dev/null +++ b/subgraphs/launchpad/tests/Launchpad.test.ts @@ -0,0 +1,395 @@ +import { BigInt } from "@graphprotocol/graph-ts"; +import { afterEach, assert, beforeEach, clearStore, test } from "matchstick-as"; +import { + handleDefaultSushiFeeBpsUpdated, + handleFeesDistributed, + handleLaunchFeesWithdrawn, + handleLaunchFeeUpdated, + handlePositionCreated, + handleProtocolRecipientUpdated, + handleProtocolReserveBpsUpdated, + handleProtocolReserveWithdrawn, + handleSushiFeeBpsUpdated, + handleTokenLaunched, +} from "../src/mappings/launchpad"; +import { + CALLER, + CHAIN_ID, + CREATOR, + CREATOR_TWO, + FACTORY, + OTHER_POOL, + POOL, + POOL_THREE, + POSITION_MANAGER, + PROTOCOL_RECIPIENT, + PROTOCOL_RECIPIENT_TWO, + QUOTE_TOKEN, + TOKEN, + TOKEN_THREE, + TOKEN_TWO, + createDefaultSushiFeeBpsUpdated, + createFeesDistributed, + createLaunchFeesWithdrawn, + createLaunchFeeUpdated, + createPositionCreated, + createProtocolReserveBpsUpdated, + createProtocolRecipientUpdated, + createProtocolReserveWithdrawn, + createSushiFeeBpsUpdated, + createTokenLaunched, + mockDeploymentContext, + mockV3Pool, +} from "./mocks"; + +const LAUNCHPAD_ID = CHAIN_ID.toString() + ":" + FACTORY.toHexString(); +const TOKEN_ID = CHAIN_ID.toString() + ":" + TOKEN.toHexString(); +const POOL_ID = CHAIN_ID.toString() + ":" + POOL.toHexString(); +const CREATOR_ID = LAUNCHPAD_ID + ":" + CREATOR.toHexString(); +const POSITION_PREFIX = + CHAIN_ID.toString() + ":" + POSITION_MANAGER.toHexString() + ":"; + +beforeEach(() => { + mockDeploymentContext(); +}); + +afterEach(() => { + clearStore(); +}); + +test("builds Launchpad, Token, Pool, and canonical initial positions", () => { + handlePositionCreated(createPositionCreated(101, 10)); + handlePositionCreated(createPositionCreated(102, 11)); + + assert.entityCount("Token", 0); + assert.entityCount("Pool", 1); + assert.fieldEquals("Pool", POOL_ID, "positionCount", "2"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "positionCount", "2"); + + handleTokenLaunched(createTokenLaunched(2, 12)); + + assert.entityCount("Launchpad", 1); + assert.entityCount("Creator", 1); + assert.entityCount("Token", 1); + assert.entityCount("Pool", 1); + assert.entityCount("LaunchPosition", 2); + + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "chainId", CHAIN_ID.toString()); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "address", + FACTORY.toHexString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "quoteToken", + QUOTE_TOKEN.toHexString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "positionManager", + POSITION_MANAGER.toHexString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "protocolRecipient", + PROTOCOL_RECIPIENT.toHexString() + ); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "launchFee", "500000000000000"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "7000"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "300"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "tokenCount", "1"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "creatorCount", "1"); + + assert.fieldEquals("Creator", CREATOR_ID, "address", CREATOR.toHexString()); + assert.fieldEquals("Creator", CREATOR_ID, "tokenCount", "1"); + + assert.fieldEquals("Token", TOKEN_ID, "address", TOKEN.toHexString()); + assert.fieldEquals("Token", TOKEN_ID, "launchpad", LAUNCHPAD_ID); + assert.fieldEquals("Token", TOKEN_ID, "creator", CREATOR_ID); + assert.fieldEquals("Token", TOKEN_ID, "pool", POOL_ID); + assert.fieldEquals("Token", TOKEN_ID, "name", "Sushi Test"); + assert.fieldEquals("Token", TOKEN_ID, "symbol", "SUSHIT"); + assert.fieldEquals("Token", TOKEN_ID, "decimals", "18"); + assert.fieldEquals("Token", TOKEN_ID, "sushiFeeBps", "7000"); + assert.fieldEquals("Token", TOKEN_ID, "reserveBps", "300"); + assert.fieldEquals("Token", TOKEN_ID, "reserveAmount", "30000000000000000"); + assert.fieldEquals("Token", TOKEN_ID, "reserveUnlockAt", "31536001"); + assert.fieldEquals("Token", TOKEN_ID, "reserveWithdrawn", "false"); + + assert.fieldEquals("Pool", POOL_ID, "token0", TOKEN.toHexString()); + assert.fieldEquals("Pool", POOL_ID, "token1", QUOTE_TOKEN.toHexString()); + assert.fieldEquals("Pool", POOL_ID, "fee", "10000"); + assert.fieldEquals("Pool", POOL_ID, "tickSpacing", "200"); + + const firstPositionId = POSITION_PREFIX + "101"; + assert.fieldEquals("LaunchPosition", firstPositionId, "pool", POOL_ID); + assert.fieldEquals("LaunchPosition", firstPositionId, "index", "0"); + assert.fieldEquals("LaunchPosition", firstPositionId, "tickLower", "-400"); + assert.fieldEquals("LaunchPosition", firstPositionId, "tickUpper", "-200"); + assert.fieldEquals( + "LaunchPosition", + firstPositionId, + "amount0Desired", + "1000" + ); + assert.fieldEquals("LaunchPosition", firstPositionId, "amount1Desired", "0"); + assert.fieldEquals("LaunchPosition", firstPositionId, "amount0", "999"); + assert.fieldEquals("LaunchPosition", firstPositionId, "amount1", "0"); +}); + +test("counts distinct creators and canonicalizes either token ordering", () => { + handlePositionCreated(createPositionCreated(101, 10)); + handleTokenLaunched(createTokenLaunched(1, 11)); + + mockV3Pool(OTHER_POOL, QUOTE_TOKEN, TOKEN_TWO); + handlePositionCreated(createPositionCreated(102, 20, OTHER_POOL, TOKEN_TWO)); + handleTokenLaunched( + createTokenLaunched(1, 21, TOKEN_TWO, OTHER_POOL, CREATOR) + ); + + mockV3Pool(POOL_THREE, QUOTE_TOKEN, TOKEN_THREE); + handlePositionCreated( + createPositionCreated(103, 30, POOL_THREE, TOKEN_THREE) + ); + handleTokenLaunched( + createTokenLaunched(1, 31, TOKEN_THREE, POOL_THREE, CREATOR_TWO) + ); + + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "tokenCount", "3"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "creatorCount", "2"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "positionCount", "3"); + assert.fieldEquals("Creator", CREATOR_ID, "tokenCount", "2"); + + const secondPoolId = CHAIN_ID.toString() + ":" + OTHER_POOL.toHexString(); + assert.fieldEquals("Pool", secondPoolId, "token0", QUOTE_TOKEN.toHexString()); + assert.fieldEquals("Pool", secondPoolId, "token1", TOKEN_TWO.toHexString()); + assert.fieldEquals( + "LaunchPosition", + POSITION_PREFIX + "102", + "amount0Desired", + "0" + ); + assert.fieldEquals( + "LaunchPosition", + POSITION_PREFIX + "102", + "amount1Desired", + "1000" + ); + assert.fieldEquals("LaunchPosition", POSITION_PREFIX + "102", "amount0", "0"); + assert.fieldEquals( + "LaunchPosition", + POSITION_PREFIX + "102", + "amount1", + "999" + ); + + handleSushiFeeBpsUpdated( + createSushiFeeBpsUpdated(7_000, 6_500, 40, TOKEN_TWO) + ); + const distribution = createFeesDistributed( + 41, + TOKEN_TWO, + OTHER_POOL, + CREATOR + ); + handleFeesDistributed(distribution); + const distributionId = + CHAIN_ID.toString() + + ":" + + distribution.transaction.hash.toHexString() + + ":41"; + assert.fieldEquals( + "FeeDistribution", + distributionId, + "amount0Collected", + "100" + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "amount1Collected", + "200" + ); +}); + +test("tracks current Launchpad defaults", () => { + handleDefaultSushiFeeBpsUpdated( + createDefaultSushiFeeBpsUpdated(7_000, 6_000, 10) + ); + handleProtocolReserveBpsUpdated( + createProtocolReserveBpsUpdated(300, 500, 11) + ); + + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "6000"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "500"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "tokenCount", "0"); +}); + +test("keeps token launch snapshots scoped to Token", () => { + handlePositionCreated(createPositionCreated(101, 10)); + handleTokenLaunched( + createTokenLaunched(1, 11, TOKEN, POOL, CREATOR, 6_000, 500) + ); + + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "7000"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "300"); + assert.fieldEquals("Token", TOKEN_ID, "sushiFeeBps", "6000"); + assert.fieldEquals("Token", TOKEN_ID, "reserveBps", "500"); +}); + +test("tracks Launchpad recipient, launch fee, and fee withdrawals", () => { + const initialFee = BigInt.fromString("500000000000000"); + const updatedFee = BigInt.fromString("1000000000000000"); + const withdrawn = BigInt.fromString("1500000000000000"); + + handleProtocolRecipientUpdated( + createProtocolRecipientUpdated( + PROTOCOL_RECIPIENT, + PROTOCOL_RECIPIENT_TWO, + 10 + ) + ); + handleLaunchFeeUpdated(createLaunchFeeUpdated(initialFee, updatedFee, 11)); + const withdrawal = createLaunchFeesWithdrawn( + PROTOCOL_RECIPIENT_TWO, + withdrawn, + 12 + ); + handleLaunchFeesWithdrawn(withdrawal); + + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "protocolRecipient", + PROTOCOL_RECIPIENT_TWO.toHexString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "launchFee", + updatedFee.toString() + ); + const withdrawalId = + CHAIN_ID.toString() + + ":" + + withdrawal.transaction.hash.toHexString() + + ":12"; + assert.fieldEquals( + "LaunchFeeWithdrawal", + withdrawalId, + "launchpad", + LAUNCHPAD_ID + ); + assert.fieldEquals( + "LaunchFeeWithdrawal", + withdrawalId, + "recipient", + PROTOCOL_RECIPIENT_TWO.toHexString() + ); + assert.fieldEquals( + "LaunchFeeWithdrawal", + withdrawalId, + "amount", + withdrawn.toString() + ); +}); + +test("tracks fee distributions and reserve withdrawals on Token", () => { + handlePositionCreated(createPositionCreated(101, 10)); + handleTokenLaunched(createTokenLaunched(1, 11)); + handleSushiFeeBpsUpdated(createSushiFeeBpsUpdated(7_000, 6_500, 12)); + + const distribution = createFeesDistributed(13); + handleFeesDistributed(distribution); + handleProtocolReserveWithdrawn(createProtocolReserveWithdrawn(14)); + + assert.fieldEquals("Token", TOKEN_ID, "sushiFeeBps", "6500"); + assert.fieldEquals("Token", TOKEN_ID, "totalAmount0Collected", "200"); + assert.fieldEquals("Token", TOKEN_ID, "totalAmount1Collected", "100"); + assert.fieldEquals("Token", TOKEN_ID, "totalAmount0ToSushi", "130"); + assert.fieldEquals("Token", TOKEN_ID, "totalAmount1ToSushi", "65"); + assert.fieldEquals("Token", TOKEN_ID, "totalAmount0ToCreator", "70"); + assert.fieldEquals("Token", TOKEN_ID, "totalAmount1ToCreator", "35"); + assert.fieldEquals("Token", TOKEN_ID, "reserveWithdrawn", "true"); + + const distributionId = + CHAIN_ID.toString() + + ":" + + distribution.transaction.hash.toHexString() + + ":13"; + assert.fieldEquals("FeeDistribution", distributionId, "token", TOKEN_ID); + assert.fieldEquals("FeeDistribution", distributionId, "pool", POOL_ID); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "caller", + CALLER.toHexString() + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "sushiRecipient", + PROTOCOL_RECIPIENT.toHexString() + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "creatorRecipient", + CREATOR.toHexString() + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "amount0Collected", + "200" + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "amount1Collected", + "100" + ); + + const withdrawalId = + CHAIN_ID.toString() + + ":" + + distribution.transaction.hash.toHexString() + + ":14"; + assert.fieldEquals("Token", TOKEN_ID, "reserveWithdrawal", withdrawalId); + assert.fieldEquals("ReserveWithdrawal", withdrawalId, "token", TOKEN_ID); + assert.fieldEquals( + "ReserveWithdrawal", + withdrawalId, + "recipient", + PROTOCOL_RECIPIENT.toHexString() + ); + assert.fieldEquals( + "ReserveWithdrawal", + withdrawalId, + "amount", + "30000000000000000" + ); +}); + +test( + "fails deterministically when ordered position identities disagree", + () => { + handlePositionCreated(createPositionCreated(101, 10)); + handlePositionCreated(createPositionCreated(102, 11, POOL, TOKEN_TWO)); + }, + true +); + +test( + "fails deterministically when the launch count does not reconcile", + () => { + handlePositionCreated(createPositionCreated(101, 10)); + handleTokenLaunched(createTokenLaunched(2, 11)); + }, + true +); diff --git a/subgraphs/launchpad/tests/mocks.ts b/subgraphs/launchpad/tests/mocks.ts new file mode 100644 index 00000000..3d08a3b5 --- /dev/null +++ b/subgraphs/launchpad/tests/mocks.ts @@ -0,0 +1,453 @@ +import { + Address, + BigInt, + DataSourceContext, + ethereum, +} from "@graphprotocol/graph-ts"; +import { + createMockedFunction, + dataSourceMock, + newMockEvent, +} from "matchstick-as"; +import { + DefaultSushiFeeBpsUpdated, + FeesDistributed, + LaunchFeesWithdrawn, + LaunchFeeUpdated, + PositionCreated, + ProtocolRecipientUpdated, + ProtocolReserveBpsUpdated, + ProtocolReserveWithdrawn, + SushiFeeBpsUpdated, + TokenLaunched, +} from "../generated/SushiLaunchpad/SushiLaunchpad"; + +export const CHAIN_ID = BigInt.fromI32(4663); +export const FACTORY = Address.fromString( + "0x1000000000000000000000000000000000000001" +); +export const CREATOR = Address.fromString( + "0x2000000000000000000000000000000000000002" +); +export const CREATOR_TWO = Address.fromString( + "0xc00000000000000000000000000000000000000c" +); +export const CALLER = Address.fromString( + "0x3000000000000000000000000000000000000003" +); +export const PROTOCOL_RECIPIENT = Address.fromString( + "0x4000000000000000000000000000000000000004" +); +export const PROTOCOL_RECIPIENT_TWO = Address.fromString( + "0xe00000000000000000000000000000000000000e" +); +export const TOKEN = Address.fromString( + "0x5000000000000000000000000000000000000005" +); +export const TOKEN_TWO = Address.fromString( + "0xa00000000000000000000000000000000000000a" +); +export const TOKEN_THREE = Address.fromString( + "0xb00000000000000000000000000000000000000b" +); +export const POOL = Address.fromString( + "0x6000000000000000000000000000000000000006" +); +export const OTHER_POOL = Address.fromString( + "0x7000000000000000000000000000000000000007" +); +export const POOL_THREE = Address.fromString( + "0xd00000000000000000000000000000000000000d" +); +export const QUOTE_TOKEN = Address.fromString( + "0x8000000000000000000000000000000000000008" +); +export const POSITION_MANAGER = Address.fromString( + "0x9000000000000000000000000000000000000009" +); + +export function mockDeploymentContext(): void { + const context = new DataSourceContext(); + context.setBigInt("chainId", CHAIN_ID); + dataSourceMock.setAddressAndContext(FACTORY.toHexString(), context); + + createMockedFunction(FACTORY, "WETH", "WETH():(address)").returns([ + ethereum.Value.fromAddress(QUOTE_TOKEN), + ]); + createMockedFunction( + FACTORY, + "positionManager", + "positionManager():(address)" + ).returns([ethereum.Value.fromAddress(POSITION_MANAGER)]); + createMockedFunction( + FACTORY, + "defaultSushiFeeBps", + "defaultSushiFeeBps():(uint16)" + ).returns([ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(7_000))]); + createMockedFunction( + FACTORY, + "protocolReserveBps", + "protocolReserveBps():(uint16)" + ).returns([ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(300))]); + createMockedFunction( + FACTORY, + "protocolRecipient", + "protocolRecipient():(address)" + ).returns([ethereum.Value.fromAddress(PROTOCOL_RECIPIENT)]); + createMockedFunction(FACTORY, "launchFee", "launchFee():(uint256)").returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromString("500000000000000")), + ]); + mockV3Pool(POOL, TOKEN, QUOTE_TOKEN); +} + +export function mockV3Pool( + pool: Address, + token0: Address, + token1: Address +): void { + createMockedFunction(pool, "token0", "token0():(address)").returns([ + ethereum.Value.fromAddress(token0), + ]); + createMockedFunction(pool, "token1", "token1():(address)").returns([ + ethereum.Value.fromAddress(token1), + ]); + createMockedFunction(pool, "fee", "fee():(uint24)").returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(10_000)), + ]); + createMockedFunction(pool, "tickSpacing", "tickSpacing():(int24)").returns([ + ethereum.Value.fromI32(200), + ]); +} + +function configureEvent(event: ethereum.Event, logIndex: i32): void { + event.address = FACTORY; + event.logIndex = BigInt.fromI32(logIndex); +} + +export function createPositionCreated( + onchainPositionId: i32, + logIndex: i32, + pool: Address = POOL, + token: Address = TOKEN +): PositionCreated { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) + ); + event.parameters.push( + new ethereum.EventParam("pool", ethereum.Value.fromAddress(pool)) + ); + event.parameters.push( + new ethereum.EventParam( + "positionId", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(onchainPositionId)) + ) + ); + event.parameters.push( + new ethereum.EventParam("tickLower", ethereum.Value.fromI32(-400)) + ); + event.parameters.push( + new ethereum.EventParam("tickUpper", ethereum.Value.fromI32(-200)) + ); + event.parameters.push( + new ethereum.EventParam( + "tokenDesired", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1_000)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "tokenUsed", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(999)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "liquidity", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(500)) + ) + ); + return event; +} + +export function createTokenLaunched( + positionCount: i32, + logIndex: i32, + token: Address = TOKEN, + pool: Address = POOL, + creator: Address = CREATOR, + initialSushiFeeBps: i32 = 7_000, + reserveBps: i32 = 300 +): TokenLaunched { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("creator", ethereum.Value.fromAddress(creator)) + ); + event.parameters.push( + new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) + ); + event.parameters.push( + new ethereum.EventParam("pool", ethereum.Value.fromAddress(pool)) + ); + event.parameters.push( + new ethereum.EventParam("name", ethereum.Value.fromString("Sushi Test")) + ); + event.parameters.push( + new ethereum.EventParam("symbol", ethereum.Value.fromString("SUSHIT")) + ); + event.parameters.push( + new ethereum.EventParam("decimals", ethereum.Value.fromI32(18)) + ); + event.parameters.push( + new ethereum.EventParam( + "totalSupply", + ethereum.Value.fromUnsignedBigInt( + BigInt.fromString("1000000000000000000") + ) + ) + ); + event.parameters.push( + new ethereum.EventParam("reserveBps", ethereum.Value.fromI32(reserveBps)) + ); + event.parameters.push( + new ethereum.EventParam( + "reserveAmount", + ethereum.Value.fromUnsignedBigInt(BigInt.fromString("30000000000000000")) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "reserveUnlockAt", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(31_536_001)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "initialSushiFeeBps", + ethereum.Value.fromI32(initialSushiFeeBps) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "positionCount", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(positionCount)) + ) + ); + return event; +} + +export function createSushiFeeBpsUpdated( + previousBps: i32, + newBps: i32, + logIndex: i32, + token: Address = TOKEN +): SushiFeeBpsUpdated { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) + ); + event.parameters.push( + new ethereum.EventParam( + "previousSushiFeeBps", + ethereum.Value.fromI32(previousBps) + ) + ); + event.parameters.push( + new ethereum.EventParam("newSushiFeeBps", ethereum.Value.fromI32(newBps)) + ); + return event; +} + +export function createDefaultSushiFeeBpsUpdated( + previousBps: i32, + newBps: i32, + logIndex: i32 +): DefaultSushiFeeBpsUpdated { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("previousBps", ethereum.Value.fromI32(previousBps)) + ); + event.parameters.push( + new ethereum.EventParam("newBps", ethereum.Value.fromI32(newBps)) + ); + return event; +} + +export function createProtocolReserveBpsUpdated( + previousBps: i32, + newBps: i32, + logIndex: i32 +): ProtocolReserveBpsUpdated { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("previousBps", ethereum.Value.fromI32(previousBps)) + ); + event.parameters.push( + new ethereum.EventParam("newBps", ethereum.Value.fromI32(newBps)) + ); + return event; +} + +export function createProtocolRecipientUpdated( + previousRecipient: Address, + newRecipient: Address, + logIndex: i32 +): ProtocolRecipientUpdated { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam( + "previousRecipient", + ethereum.Value.fromAddress(previousRecipient) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "newRecipient", + ethereum.Value.fromAddress(newRecipient) + ) + ); + return event; +} + +export function createLaunchFeeUpdated( + previousFee: BigInt, + newFee: BigInt, + logIndex: i32 +): LaunchFeeUpdated { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam( + "previousFee", + ethereum.Value.fromUnsignedBigInt(previousFee) + ) + ); + event.parameters.push( + new ethereum.EventParam("newFee", ethereum.Value.fromUnsignedBigInt(newFee)) + ); + return event; +} + +export function createLaunchFeesWithdrawn( + recipient: Address, + amount: BigInt, + logIndex: i32 +): LaunchFeesWithdrawn { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("recipient", ethereum.Value.fromAddress(recipient)) + ); + event.parameters.push( + new ethereum.EventParam("amount", ethereum.Value.fromUnsignedBigInt(amount)) + ); + return event; +} + +export function createFeesDistributed( + logIndex: i32, + token: Address = TOKEN, + pool: Address = POOL, + creator: Address = CREATOR +): FeesDistributed { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("caller", ethereum.Value.fromAddress(CALLER)) + ); + event.parameters.push( + new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) + ); + event.parameters.push( + new ethereum.EventParam("pool", ethereum.Value.fromAddress(pool)) + ); + event.parameters.push( + new ethereum.EventParam("creator", ethereum.Value.fromAddress(creator)) + ); + event.parameters.push( + new ethereum.EventParam( + "protocolRecipient", + ethereum.Value.fromAddress(PROTOCOL_RECIPIENT) + ) + ); + event.parameters.push( + new ethereum.EventParam("sushiFeeBps", ethereum.Value.fromI32(6_500)) + ); + event.parameters.push( + new ethereum.EventParam( + "wethCollected", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(100)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "tokenCollected", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(200)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "wethToSushi", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(65)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "tokenToSushi", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(130)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "wethToCreator", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(35)) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "tokenToCreator", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(70)) + ) + ); + return event; +} + +export function createProtocolReserveWithdrawn( + logIndex: i32, + token: Address = TOKEN +): ProtocolReserveWithdrawn { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) + ); + event.parameters.push( + new ethereum.EventParam( + "recipient", + ethereum.Value.fromAddress(PROTOCOL_RECIPIENT) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "amount", + ethereum.Value.fromUnsignedBigInt(BigInt.fromString("30000000000000000")) + ) + ); + return event; +} diff --git a/subgraphs/launchpad/tsconfig.json b/subgraphs/launchpad/tsconfig.json new file mode 100644 index 00000000..8d9f5d71 --- /dev/null +++ b/subgraphs/launchpad/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../config/tsconfig.json", + "include": ["src", "generated", "tests"], + "compilerOptions": { + "baseUrl": ".", + "lib": ["ES2016"], + "rootDirs": ["./src", "./generated", "./tests"] + } +} From ef7d296d56002a9b68303ab8503f5d92b095098d Mon Sep 17 00:00:00 2001 From: LufyCZ Date: Wed, 22 Jul 2026 12:54:44 +0000 Subject: [PATCH 3/7] feat: launchpad e2e --- .gitignore | 5 +- pnpm-lock.yaml | 492 +++++++-- subgraphs/launchpad/README.md | 61 +- subgraphs/launchpad/e2e/docker-compose.yml | 61 ++ subgraphs/launchpad/e2e/hardhat.config.ts | 13 + subgraphs/launchpad/e2e/model.ts | 413 ++++++++ subgraphs/launchpad/e2e/run.ts | 1081 ++++++++++++++++++++ subgraphs/launchpad/e2e/scenario.test.ts | 34 + subgraphs/launchpad/e2e/scenario.ts | 343 +++++++ subgraphs/launchpad/e2e/tsconfig.json | 13 + subgraphs/launchpad/package.json | 9 +- 11 files changed, 2462 insertions(+), 63 deletions(-) create mode 100644 subgraphs/launchpad/e2e/docker-compose.yml create mode 100644 subgraphs/launchpad/e2e/hardhat.config.ts create mode 100644 subgraphs/launchpad/e2e/model.ts create mode 100644 subgraphs/launchpad/e2e/run.ts create mode 100644 subgraphs/launchpad/e2e/scenario.test.ts create mode 100644 subgraphs/launchpad/e2e/scenario.ts create mode 100644 subgraphs/launchpad/e2e/tsconfig.json diff --git a/.gitignore b/.gitignore index fe32fcce..bb47aaa3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ deploy.sh # turbo .turbo build/** -dist/** \ No newline at end of file +dist/** + +# Local subgraph E2E diagnostics +subgraphs/launchpad/.e2e-artifacts/ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9727b649..d550eb24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,7 +33,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -51,7 +51,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -69,7 +69,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -90,16 +90,28 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 + '@types/node': + specifier: ^22.13.11 + version: 22.13.11 assemblyscript: specifier: ^0.19.20 version: 0.19.23 + ethers: + specifier: ^6.14.0 + version: 6.17.0 matchstick-as: specifier: 0.6.0 version: 0.6.0 + tsx: + specifier: ^4.20.3 + version: 4.23.1 + typescript: + specifier: ^5.8.2 + version: 5.8.2 wabt: specifier: 1.0.24 version: 1.0.24 @@ -108,7 +120,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -129,7 +141,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -147,7 +159,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -168,7 +180,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -189,7 +201,7 @@ importers: devDependencies: '@graphprotocol/graph-cli': specifier: ^0.96.0 - version: 0.96.0(typescript@5.8.2) + version: 0.96.0(@types/node@22.13.11)(typescript@5.8.2) '@graphprotocol/graph-ts': specifier: ^0.27.0 version: 0.27.0 @@ -208,6 +220,10 @@ importers: packages: + /@adraffy/ens-normalize@1.11.1: + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + dev: true + /@babel/code-frame@7.26.2: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -232,6 +248,240 @@ packages: '@chainsafe/is-ip': 2.1.0 dev: true + /@esbuild/aix-ppc64@0.28.1: + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.28.1: + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.28.1: + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.28.1: + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.28.1: + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.28.1: + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.28.1: + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.28.1: + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.28.1: + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.28.1: + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.28.1: + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.28.1: + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.28.1: + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.28.1: + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.28.1: + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.28.1: + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.28.1: + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-arm64@0.28.1: + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.28.1: + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-arm64@0.28.1: + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.28.1: + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openharmony-arm64@0.28.1: + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.28.1: + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.28.1: + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.28.1: + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.28.1: + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@float-capital/float-subgraph-uncrashable@0.0.0-internal-testing.5: resolution: {integrity: sha512-yZ0H5e3EpAYKokX/AbtplzlvSxEJY7ZfpvQyDzyODkks0hakAAlDG6fQu1SlDJMWorY7bbq1j7fCiFeTWci6TA==} hasBin: true @@ -242,7 +492,7 @@ packages: js-yaml: 4.1.0 dev: true - /@graphprotocol/graph-cli@0.96.0(typescript@5.8.2): + /@graphprotocol/graph-cli@0.96.0(@types/node@22.13.11)(typescript@5.8.2): resolution: {integrity: sha512-zjVMtzoipxj5tBEAXFBVAIHT6kkxMYauXHX00/IoTu3pE26hdt4ogV1vbfBai/TnPkQCDR86vJiN0XqPfc+7sw==} engines: {node: '>=20.18.1'} hasBin: true @@ -250,7 +500,7 @@ packages: '@float-capital/float-subgraph-uncrashable': 0.0.0-internal-testing.5 '@oclif/core': 4.2.6 '@oclif/plugin-autocomplete': 3.2.26 - '@oclif/plugin-not-found': 3.2.47 + '@oclif/plugin-not-found': 3.2.47(@types/node@22.13.11) '@oclif/plugin-warn-if-update-available': 3.1.38 '@pinax/graph-networks-registry': 0.6.7 '@whatwg-node/fetch': 0.10.5 @@ -288,7 +538,7 @@ packages: assemblyscript: 0.19.10 dev: true - /@inquirer/checkbox@4.1.4: + /@inquirer/checkbox@4.1.4(@types/node@22.13.11): resolution: {integrity: sha512-d30576EZdApjAMceijXA5jDzRQHT/MygbC+J8I7EqA6f/FRpYxlRtRJbHF8gHeWYeSdOuTEJqonn7QLB1ELezA==} engines: {node: '>=18'} peerDependencies: @@ -297,14 +547,15 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 + '@inquirer/core': 10.1.9(@types/node@22.13.11) '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5 + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 dev: true - /@inquirer/confirm@5.1.8: + /@inquirer/confirm@5.1.8(@types/node@22.13.11): resolution: {integrity: sha512-dNLWCYZvXDjO3rnQfk2iuJNL4Ivwz/T2+C3+WnNfJKsNGSuOs3wAo2F6e0p946gtSAk31nZMfW+MRmYaplPKsg==} engines: {node: '>=18'} peerDependencies: @@ -313,11 +564,12 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 - '@inquirer/type': 3.0.5 + '@inquirer/core': 10.1.9(@types/node@22.13.11) + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 dev: true - /@inquirer/core@10.1.9: + /@inquirer/core@10.1.9(@types/node@22.13.11): resolution: {integrity: sha512-sXhVB8n20NYkUBfDYgizGHlpRVaCRjtuzNZA6xpALIUbkgfd2Hjz+DfEN6+h1BRnuxw0/P4jCIMjMsEOAMwAJw==} engines: {node: '>=18'} peerDependencies: @@ -327,7 +579,8 @@ packages: optional: true dependencies: '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5 + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 ansi-escapes: 4.3.2 cli-width: 4.1.0 mute-stream: 2.0.0 @@ -336,7 +589,7 @@ packages: yoctocolors-cjs: 2.1.2 dev: true - /@inquirer/editor@4.2.9: + /@inquirer/editor@4.2.9(@types/node@22.13.11): resolution: {integrity: sha512-8HjOppAxO7O4wV1ETUlJFg6NDjp/W2NP5FB9ZPAcinAlNT4ZIWOLe2pUVwmmPRSV0NMdI5r/+lflN55AwZOKSw==} engines: {node: '>=18'} peerDependencies: @@ -345,12 +598,13 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 - '@inquirer/type': 3.0.5 + '@inquirer/core': 10.1.9(@types/node@22.13.11) + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 external-editor: 3.1.0 dev: true - /@inquirer/expand@4.0.11: + /@inquirer/expand@4.0.11(@types/node@22.13.11): resolution: {integrity: sha512-OZSUW4hFMW2TYvX/Sv+NnOZgO8CHT2TU1roUCUIF2T+wfw60XFRRp9MRUPCT06cRnKL+aemt2YmTWwt7rOrNEA==} engines: {node: '>=18'} peerDependencies: @@ -359,8 +613,9 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 - '@inquirer/type': 3.0.5 + '@inquirer/core': 10.1.9(@types/node@22.13.11) + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 yoctocolors-cjs: 2.1.2 dev: true @@ -369,7 +624,7 @@ packages: engines: {node: '>=18'} dev: true - /@inquirer/input@4.1.8: + /@inquirer/input@4.1.8(@types/node@22.13.11): resolution: {integrity: sha512-WXJI16oOZ3/LiENCAxe8joniNp8MQxF6Wi5V+EBbVA0ZIOpFcL4I9e7f7cXse0HJeIPCWO8Lcgnk98juItCi7Q==} engines: {node: '>=18'} peerDependencies: @@ -378,11 +633,12 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 - '@inquirer/type': 3.0.5 + '@inquirer/core': 10.1.9(@types/node@22.13.11) + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 dev: true - /@inquirer/number@3.0.11: + /@inquirer/number@3.0.11(@types/node@22.13.11): resolution: {integrity: sha512-pQK68CsKOgwvU2eA53AG/4npRTH2pvs/pZ2bFvzpBhrznh8Mcwt19c+nMO7LHRr3Vreu1KPhNBF3vQAKrjIulw==} engines: {node: '>=18'} peerDependencies: @@ -391,11 +647,12 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 - '@inquirer/type': 3.0.5 + '@inquirer/core': 10.1.9(@types/node@22.13.11) + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 dev: true - /@inquirer/password@4.0.11: + /@inquirer/password@4.0.11(@types/node@22.13.11): resolution: {integrity: sha512-dH6zLdv+HEv1nBs96Case6eppkRggMe8LoOTl30+Gq5Wf27AO/vHFgStTVz4aoevLdNXqwE23++IXGw4eiOXTg==} engines: {node: '>=18'} peerDependencies: @@ -404,12 +661,13 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 - '@inquirer/type': 3.0.5 + '@inquirer/core': 10.1.9(@types/node@22.13.11) + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 ansi-escapes: 4.3.2 dev: true - /@inquirer/prompts@7.4.0: + /@inquirer/prompts@7.4.0(@types/node@22.13.11): resolution: {integrity: sha512-EZiJidQOT4O5PYtqnu1JbF0clv36oW2CviR66c7ma4LsupmmQlUwmdReGKRp456OWPWMz3PdrPiYg3aCk3op2w==} engines: {node: '>=18'} peerDependencies: @@ -418,19 +676,20 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/checkbox': 4.1.4 - '@inquirer/confirm': 5.1.8 - '@inquirer/editor': 4.2.9 - '@inquirer/expand': 4.0.11 - '@inquirer/input': 4.1.8 - '@inquirer/number': 3.0.11 - '@inquirer/password': 4.0.11 - '@inquirer/rawlist': 4.0.11 - '@inquirer/search': 3.0.11 - '@inquirer/select': 4.1.0 + '@inquirer/checkbox': 4.1.4(@types/node@22.13.11) + '@inquirer/confirm': 5.1.8(@types/node@22.13.11) + '@inquirer/editor': 4.2.9(@types/node@22.13.11) + '@inquirer/expand': 4.0.11(@types/node@22.13.11) + '@inquirer/input': 4.1.8(@types/node@22.13.11) + '@inquirer/number': 3.0.11(@types/node@22.13.11) + '@inquirer/password': 4.0.11(@types/node@22.13.11) + '@inquirer/rawlist': 4.0.11(@types/node@22.13.11) + '@inquirer/search': 3.0.11(@types/node@22.13.11) + '@inquirer/select': 4.1.0(@types/node@22.13.11) + '@types/node': 22.13.11 dev: true - /@inquirer/rawlist@4.0.11: + /@inquirer/rawlist@4.0.11(@types/node@22.13.11): resolution: {integrity: sha512-uAYtTx0IF/PqUAvsRrF3xvnxJV516wmR6YVONOmCWJbbt87HcDHLfL9wmBQFbNJRv5kCjdYKrZcavDkH3sVJPg==} engines: {node: '>=18'} peerDependencies: @@ -439,12 +698,13 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 - '@inquirer/type': 3.0.5 + '@inquirer/core': 10.1.9(@types/node@22.13.11) + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 yoctocolors-cjs: 2.1.2 dev: true - /@inquirer/search@3.0.11: + /@inquirer/search@3.0.11(@types/node@22.13.11): resolution: {integrity: sha512-9CWQT0ikYcg6Ls3TOa7jljsD7PgjcsYEM0bYE+Gkz+uoW9u8eaJCRHJKkucpRE5+xKtaaDbrND+nPDoxzjYyew==} engines: {node: '>=18'} peerDependencies: @@ -453,13 +713,14 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 + '@inquirer/core': 10.1.9(@types/node@22.13.11) '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5 + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 yoctocolors-cjs: 2.1.2 dev: true - /@inquirer/select@4.1.0: + /@inquirer/select@4.1.0(@types/node@22.13.11): resolution: {integrity: sha512-z0a2fmgTSRN+YBuiK1ROfJ2Nvrpij5lVN3gPDkQGhavdvIVGHGW29LwYZfM/j42Ai2hUghTI/uoBuTbrJk42bA==} engines: {node: '>=18'} peerDependencies: @@ -468,14 +729,15 @@ packages: '@types/node': optional: true dependencies: - '@inquirer/core': 10.1.9 + '@inquirer/core': 10.1.9(@types/node@22.13.11) '@inquirer/figures': 1.0.11 - '@inquirer/type': 3.0.5 + '@inquirer/type': 3.0.5(@types/node@22.13.11) + '@types/node': 22.13.11 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 dev: true - /@inquirer/type@3.0.5: + /@inquirer/type@3.0.5(@types/node@22.13.11): resolution: {integrity: sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==} engines: {node: '>=18'} peerDependencies: @@ -483,6 +745,8 @@ packages: peerDependenciesMeta: '@types/node': optional: true + dependencies: + '@types/node': 22.13.11 dev: true /@ipld/dag-cbor@9.2.2: @@ -595,6 +859,12 @@ packages: uint8arrays: 5.1.0 dev: true + /@noble/curves@1.2.0: + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + dependencies: + '@noble/hashes': 1.3.2 + dev: true + /@noble/curves@1.4.2: resolution: {integrity: sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==} dependencies: @@ -608,6 +878,11 @@ packages: '@noble/hashes': 1.7.1 dev: true + /@noble/hashes@1.3.2: + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + dev: true + /@noble/hashes@1.4.0: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -675,11 +950,11 @@ packages: - supports-color dev: true - /@oclif/plugin-not-found@3.2.47: + /@oclif/plugin-not-found@3.2.47(@types/node@22.13.11): resolution: {integrity: sha512-7Zk10TQhPOd5kkS4wiLDBqtR2MRM8FBiSrZDmSsgME1kXt4eYQLAFc9c5WJzkpglNb6g5/iD1LvbhTNd5oLe1g==} engines: {node: '>=18.0.0'} dependencies: - '@inquirer/prompts': 7.4.0 + '@inquirer/prompts': 7.4.0(@types/node@22.13.11) '@oclif/core': 4.2.6 ansis: 3.17.0 fast-levenshtein: 3.0.0 @@ -752,7 +1027,7 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 12.20.55 + '@types/node': 22.13.11 dev: true /@types/dns-packet@5.6.5: @@ -771,6 +1046,12 @@ packages: undici-types: 6.20.0 dev: true + /@types/node@22.7.5: + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + dependencies: + undici-types: 6.19.8 + dev: true + /@types/parse-json@4.0.2: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} dev: true @@ -778,7 +1059,7 @@ packages: /@types/ws@7.4.7: resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} dependencies: - '@types/node': 12.20.55 + '@types/node': 22.13.11 dev: true /@whatwg-node/disposablestack@0.0.6: @@ -834,6 +1115,10 @@ packages: typescript: 5.8.2 dev: true + /aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + dev: true + /ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1376,6 +1661,40 @@ packages: es6-promise: 4.2.8 dev: true + /esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + dev: true + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1395,6 +1714,22 @@ packages: '@scure/bip39': 1.3.0 dev: true + /ethers@6.17.0: + resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==} + engines: {node: '>=14.0.0'} + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + /eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} dev: true @@ -1520,6 +1855,14 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} dev: true @@ -2780,10 +3123,24 @@ packages: is-number: 7.0.0 dev: true + /tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + dev: true + /tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} dev: true + /tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} dependencies: @@ -2880,6 +3237,10 @@ packages: multiformats: 13.3.2 dev: true + /undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + dev: true + /undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} dev: true @@ -3066,6 +3427,19 @@ packages: optional: true dev: true + /ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true diff --git a/subgraphs/launchpad/README.md b/subgraphs/launchpad/README.md index 16b6c5d9..5dfaa979 100644 --- a/subgraphs/launchpad/README.md +++ b/subgraphs/launchpad/README.md @@ -61,8 +61,9 @@ generated ABI bindings, while later entries use unique names such as `SushiLaunchpadV2`. The mapping is split by responsibility: `helpers.ts` owns chain-scoped IDs and -entity loading, `token.ts` owns token/pool/position and token-level flows, and -`launchpad.ts` owns factory-wide settings and provides the manifest exports. +entity loading, `pool.ts` owns pool discovery and initial launch positions, +`token.ts` owns token-level flows, and `launchpad.ts` owns factory-wide settings +and provides the manifest exports. Robinhood currently contains a zero-address build placeholder because the production factory address and deployment block are still an explicit launch @@ -79,3 +80,59 @@ pnpm test `abis/SushiLaunchpad.json` is pinned from the current contract artifact in the `sushi-launchpad` repository. Refresh it together with mappings and tests when an indexed event changes. + +## Local end-to-end test + +The E2E harness runs the real Launchpad and launched token contracts against a +Hardhat chain, indexes their events with a local Graph Node, and compares every +entity with an independently constructed expected projection. The Sushi V3 +factory, pools, position manager, and WETH use the contract repository's +behavioral mocks so fee collection and canonical token ordering remain fast and +deterministic. + +Prerequisites: + +- Node.js 24 and pnpm/Corepack; +- Docker with either the Compose plugin or the `docker-compose` command; +- a sibling `sushi-launchpad` checkout with dependencies installed. From this + package, the default location resolves to `../../../sushi-launchpad`. + +Run the default deterministic matrix: + +```sh +pnpm test:e2e +``` + +The deterministic planner and Node.js harness can be checked without starting +Docker or Hardhat: + +```sh +pnpm test:e2e:planner +pnpm test:e2e:typecheck +``` + +Reproduce one scenario or point to a different contract checkout: + +```sh +pnpm test:e2e -- --seed 202 +LAUNCHPAD_CONTRACTS_DIR=/path/to/sushi-launchpad pnpm test:e2e -- --seed 202 +``` + +The default seeds are `101`, `202`, and `303`. Each scenario is fully prepared +before transactions are sent and varies actors, launch ranges, fees, canonical +token ordering, and valid action ordering while guaranteeing coverage of every +indexed lifecycle event. The first launch and factory-default changes are mined +in one block to exercise Graph Node's block-visible contract-call semantics. + +The runner requires ports `8545`, `8000`, `8001`, `8020`, `8030`, `8040`, +`5001`, and `5432`. It fails before startup if any are occupied. On success or +failure it stops Hardhat and removes the E2E containers and volumes. Plans, +transaction traces, GraphQL snapshots, expected projections, and service logs +are retained in the ignored `.e2e-artifacts/` directory; the failing seed and +artifact path are printed for reproduction. + +The harness uses its own minimal Hardhat node configuration targeting Cancun +with a 60M block and transaction gas cap. Cancun matches the contracts' compile +target while avoiding newer per-transaction protocol caps below Graph Node's +50M historical contract-call allowance. The contract repository's development +and production config remains unchanged. diff --git a/subgraphs/launchpad/e2e/docker-compose.yml b/subgraphs/launchpad/e2e/docker-compose.yml new file mode 100644 index 00000000..253bf0c5 --- /dev/null +++ b/subgraphs/launchpad/e2e/docker-compose.yml @@ -0,0 +1,61 @@ +services: + graph-node: + image: graphprotocol/graph-node:v0.41.2@sha256:5dd0622b0bcb52ce581911cf25ade5dc3c7c3d66638d2acab713cefa6d7f76f8 + ports: + - "127.0.0.1:8000:8000" + - "127.0.0.1:8001:8001" + - "127.0.0.1:8020:8020" + - "127.0.0.1:8030:8030" + - "127.0.0.1:8040:8040" + depends_on: + ipfs: + condition: service_healthy + postgres: + condition: service_healthy + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + postgres_host: postgres + postgres_user: graph-node + postgres_pass: launchpad-e2e + postgres_db: graph-node + ipfs: "ipfs:5001" + ethereum: "hardhat:http://host.docker.internal:8545" + GRAPH_LOG: info + + ipfs: + image: ipfs/kubo:v0.17.0@sha256:803fac58ba15bd763b97a1ce17bca57f75348f2088d384a908b77e8540f35560 + ports: + - "127.0.0.1:5001:5001" + healthcheck: + test: ["CMD", "ipfs", "id"] + interval: 2s + timeout: 5s + retries: 30 + volumes: + - ipfs-data:/data/ipfs + + postgres: + image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 + ports: + - "127.0.0.1:5432:5432" + command: + - postgres + - -cshared_preload_libraries=pg_stat_statements + - -cmax_connections=200 + environment: + POSTGRES_USER: graph-node + POSTGRES_PASSWORD: launchpad-e2e + POSTGRES_DB: graph-node + POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U graph-node -d graph-node"] + interval: 2s + timeout: 5s + retries: 30 + volumes: + - postgres-data:/var/lib/postgresql/data + +volumes: + ipfs-data: + postgres-data: diff --git a/subgraphs/launchpad/e2e/hardhat.config.ts b/subgraphs/launchpad/e2e/hardhat.config.ts new file mode 100644 index 00000000..d4b90d84 --- /dev/null +++ b/subgraphs/launchpad/e2e/hardhat.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "hardhat/config"; + +export default defineConfig({ + networks: { + hardhatMainnet: { + type: "edr-simulated", + chainType: "l1", + hardfork: "cancun", + blockGasLimit: 60_000_000, + transactionGasCap: 60_000_000, + }, + }, +}); diff --git a/subgraphs/launchpad/e2e/model.ts b/subgraphs/launchpad/e2e/model.ts new file mode 100644 index 00000000..75236f97 --- /dev/null +++ b/subgraphs/launchpad/e2e/model.ts @@ -0,0 +1,413 @@ +import assert from "node:assert/strict"; + +export interface IndexedEvent { + name: string; + args: Record; + transactionHash: string; + logIndex: number; + blockNumber: number; + blockHash: string; + timestamp: number; +} + +export interface PoolConfiguration { + address: string; + token0: string; + token1: string; + fee: number; + tickSpacing: number; +} + +export interface ExpectedModelInput { + chainId: bigint; + launchpadAddress: string; + quoteToken: string; + positionManager: string; + protocolRecipient: string; + launchFee: bigint; + defaultSushiFeeBps: number; + protocolReserveBps: number; + events: IndexedEvent[]; + pools: Map; +} + +type Entity = Record & { id: string }; + +export interface EntitySnapshot { + launchpads: Entity[]; + creators: Entity[]; + tokens: Entity[]; + pools: Entity[]; + launchPositions: Entity[]; + feeDistributions: Entity[]; + reserveWithdrawals: Entity[]; + launchFeeWithdrawals: Entity[]; +} + +const lower = (value: unknown): string => String(value).toLowerCase(); +const decimal = (value: unknown): string => BigInt(value as bigint).toString(); +const integer = (value: unknown): number => Number(value); +const relation = (id: string): { id: string } => ({ id }); + +function addressId(chainId: bigint, address: unknown): string { + return `${chainId}:${lower(address)}`; +} + +function eventId(chainId: bigint, event: IndexedEvent): string { + return `${chainId}:${event.transactionHash.toLowerCase()}:${event.logIndex}`; +} + +function positionId( + chainId: bigint, + positionManager: string, + onchainPositionId: unknown +): string { + return `${chainId}:${positionManager.toLowerCase()}:${decimal( + onchainPositionId + )}`; +} + +function metadata(event: IndexedEvent): Record { + return { + transactionHash: event.transactionHash.toLowerCase(), + logIndex: event.logIndex.toString(), + blockNumber: event.blockNumber.toString(), + blockHash: event.blockHash.toLowerCase(), + timestamp: event.timestamp.toString(), + }; +} + +function values(entities: Map): T[] { + return [...entities.values()]; +} + +function add(value: unknown, amount: unknown): string { + return (BigInt(String(value)) + BigInt(amount as bigint)).toString(); +} + +export function buildExpectedSnapshot( + input: ExpectedModelInput +): EntitySnapshot { + const launchpadId = addressId(input.chainId, input.launchpadAddress); + const creators = new Map(); + const tokens = new Map(); + const pools = new Map(); + const launchPositions = new Map(); + const feeDistributions = new Map(); + const reserveWithdrawals = new Map(); + const launchFeeWithdrawals = new Map(); + + const launchpad: Entity = { + id: launchpadId, + chainId: input.chainId.toString(), + address: input.launchpadAddress.toLowerCase(), + quoteToken: input.quoteToken.toLowerCase(), + positionManager: input.positionManager.toLowerCase(), + protocolRecipient: input.protocolRecipient.toLowerCase(), + launchFee: input.launchFee.toString(), + defaultSushiFeeBps: input.defaultSushiFeeBps, + protocolReserveBps: input.protocolReserveBps, + tokenCount: 0, + creatorCount: 0, + positionCount: 0, + tokens: [], + pools: [], + creators: [], + launchFeeWithdrawals: [], + }; + + const orderedEvents = [...input.events].sort( + (left, right) => + left.blockNumber - right.blockNumber || left.logIndex - right.logIndex + ); + + for (const event of orderedEvents) { + const args = event.args; + + if (event.name === "PositionCreated") { + const poolAddress = lower(args.pool); + const poolConfiguration = input.pools.get(poolAddress); + assert( + poolConfiguration, + `Missing pool configuration for ${poolAddress}` + ); + const idForPool = addressId(input.chainId, poolAddress); + let pool = pools.get(idForPool); + if (!pool) { + pool = { + id: idForPool, + chainId: input.chainId.toString(), + address: poolAddress, + launchpad: relation(launchpadId), + token0: poolConfiguration.token0.toLowerCase(), + token1: poolConfiguration.token1.toLowerCase(), + fee: poolConfiguration.fee, + tickSpacing: poolConfiguration.tickSpacing, + positionManager: input.positionManager.toLowerCase(), + positionCount: 0, + creationTransactionHash: event.transactionHash.toLowerCase(), + creationBlockNumber: event.blockNumber.toString(), + creationBlockHash: event.blockHash.toLowerCase(), + createdAt: event.timestamp.toString(), + positions: [], + }; + pools.set(idForPool, pool); + } + + const tokenIs0 = + poolConfiguration.token0.toLowerCase() === lower(args.token); + const idForPosition = positionId( + input.chainId, + input.positionManager, + args.positionId + ); + const index = pool.positionCount as number; + const zero = "0"; + const desired = decimal(args.tokenDesired); + const used = decimal(args.tokenUsed); + const position: Entity = { + id: idForPosition, + chainId: input.chainId.toString(), + positionManager: input.positionManager.toLowerCase(), + positionId: decimal(args.positionId), + index, + pool: relation(idForPool), + tickLower: integer(args.tickLower), + tickUpper: integer(args.tickUpper), + liquidity: decimal(args.liquidity), + amount0Desired: tokenIs0 ? desired : zero, + amount1Desired: tokenIs0 ? zero : desired, + amount0: tokenIs0 ? used : zero, + amount1: tokenIs0 ? zero : used, + creationTransactionHash: event.transactionHash.toLowerCase(), + creationLogIndex: event.logIndex.toString(), + creationBlockNumber: event.blockNumber.toString(), + creationBlockHash: event.blockHash.toLowerCase(), + createdAt: event.timestamp.toString(), + }; + launchPositions.set(idForPosition, position); + (pool.positions as Array<{ id: string }>).push(relation(idForPosition)); + pool.positionCount = index + 1; + launchpad.positionCount = (launchpad.positionCount as number) + 1; + continue; + } + + if (event.name === "TokenLaunched") { + const idForToken = addressId(input.chainId, args.token); + const idForCreator = `${launchpadId}:${lower(args.creator)}`; + const idForPool = addressId(input.chainId, args.pool); + let creator = creators.get(idForCreator); + if (!creator) { + creator = { + id: idForCreator, + chainId: input.chainId.toString(), + address: lower(args.creator), + launchpad: relation(launchpadId), + tokenCount: 0, + tokens: [], + }; + creators.set(idForCreator, creator); + launchpad.creatorCount = (launchpad.creatorCount as number) + 1; + } + creator.tokenCount = (creator.tokenCount as number) + 1; + (creator.tokens as Array<{ id: string }>).push(relation(idForToken)); + + const token: Entity = { + id: idForToken, + chainId: input.chainId.toString(), + address: lower(args.token), + launchpad: relation(launchpadId), + creator: relation(idForCreator), + pool: relation(idForPool), + name: String(args.name), + symbol: String(args.symbol), + decimals: integer(args.decimals), + totalSupply: decimal(args.totalSupply), + sushiFeeBps: integer(args.initialSushiFeeBps), + reserveBps: integer(args.reserveBps), + reserveAmount: decimal(args.reserveAmount), + reserveUnlockAt: decimal(args.reserveUnlockAt), + reserveWithdrawn: false, + reserveWithdrawal: null, + totalAmount0Collected: "0", + totalAmount1Collected: "0", + totalAmount0ToSushi: "0", + totalAmount1ToSushi: "0", + totalAmount0ToCreator: "0", + totalAmount1ToCreator: "0", + feeDistributions: [], + creationTransactionHash: event.transactionHash.toLowerCase(), + creationLogIndex: event.logIndex.toString(), + creationBlockNumber: event.blockNumber.toString(), + creationBlockHash: event.blockHash.toLowerCase(), + createdAt: event.timestamp.toString(), + }; + tokens.set(idForToken, token); + launchpad.tokenCount = (launchpad.tokenCount as number) + 1; + continue; + } + + if (event.name === "SushiFeeBpsUpdated") { + const token = tokens.get(addressId(input.chainId, args.token)); + assert( + token, + `Unknown token in SushiFeeBpsUpdated: ${lower(args.token)}` + ); + token.sushiFeeBps = integer(args.newSushiFeeBps); + continue; + } + + if (event.name === "FeesDistributed") { + const idForToken = addressId(input.chainId, args.token); + const idForPool = addressId(input.chainId, args.pool); + const token = tokens.get(idForToken); + const pool = pools.get(idForPool); + assert(token && pool, `Unknown fee distribution target ${idForToken}`); + const id = eventId(input.chainId, event); + const tokenIs0 = lower(pool.token0) === lower(args.token); + const amount0Collected = tokenIs0 + ? decimal(args.tokenCollected) + : decimal(args.wethCollected); + const amount1Collected = tokenIs0 + ? decimal(args.wethCollected) + : decimal(args.tokenCollected); + const amount0ToSushi = tokenIs0 + ? decimal(args.tokenToSushi) + : decimal(args.wethToSushi); + const amount1ToSushi = tokenIs0 + ? decimal(args.wethToSushi) + : decimal(args.tokenToSushi); + const amount0ToCreator = tokenIs0 + ? decimal(args.tokenToCreator) + : decimal(args.wethToCreator); + const amount1ToCreator = tokenIs0 + ? decimal(args.wethToCreator) + : decimal(args.tokenToCreator); + const distribution: Entity = { + id, + chainId: input.chainId.toString(), + token: relation(idForToken), + pool: relation(idForPool), + caller: lower(args.caller), + sushiRecipient: lower(args.protocolRecipient), + creatorRecipient: lower(args.creator), + sushiFeeBps: integer(args.sushiFeeBps), + amount0Collected, + amount1Collected, + amount0ToSushi, + amount1ToSushi, + amount0ToCreator, + amount1ToCreator, + ...metadata(event), + }; + feeDistributions.set(id, distribution); + (token.feeDistributions as Array<{ id: string }>).push(relation(id)); + token.totalAmount0Collected = add( + token.totalAmount0Collected, + amount0Collected + ); + token.totalAmount1Collected = add( + token.totalAmount1Collected, + amount1Collected + ); + token.totalAmount0ToSushi = add( + token.totalAmount0ToSushi, + amount0ToSushi + ); + token.totalAmount1ToSushi = add( + token.totalAmount1ToSushi, + amount1ToSushi + ); + token.totalAmount0ToCreator = add( + token.totalAmount0ToCreator, + amount0ToCreator + ); + token.totalAmount1ToCreator = add( + token.totalAmount1ToCreator, + amount1ToCreator + ); + continue; + } + + if (event.name === "ProtocolReserveWithdrawn") { + const idForToken = addressId(input.chainId, args.token); + const token = tokens.get(idForToken); + assert(token, `Unknown reserve withdrawal target ${idForToken}`); + const id = eventId(input.chainId, event); + const withdrawal: Entity = { + id, + chainId: input.chainId.toString(), + token: relation(idForToken), + recipient: lower(args.recipient), + amount: decimal(args.amount), + ...metadata(event), + }; + reserveWithdrawals.set(id, withdrawal); + token.reserveWithdrawn = true; + token.reserveWithdrawal = relation(id); + continue; + } + + if (event.name === "LaunchFeesWithdrawn") { + const id = eventId(input.chainId, event); + const withdrawal: Entity = { + id, + chainId: input.chainId.toString(), + launchpad: relation(launchpadId), + recipient: lower(args.recipient), + amount: decimal(args.amount), + ...metadata(event), + }; + launchFeeWithdrawals.set(id, withdrawal); + } + } + + launchpad.tokens = values(tokens).map((entity) => relation(entity.id)); + launchpad.pools = values(pools).map((entity) => relation(entity.id)); + launchpad.creators = values(creators).map((entity) => relation(entity.id)); + launchpad.launchFeeWithdrawals = values(launchFeeWithdrawals).map((entity) => + relation(entity.id) + ); + + return normalizeSnapshot({ + launchpads: [launchpad], + creators: values(creators), + tokens: values(tokens), + pools: values(pools), + launchPositions: values(launchPositions), + feeDistributions: values(feeDistributions), + reserveWithdrawals: values(reserveWithdrawals), + launchFeeWithdrawals: values(launchFeeWithdrawals), + }); +} + +function normalizeValue(value: unknown): unknown { + if (Array.isArray(value)) { + const normalized = value.map(normalizeValue); + if ( + normalized.every( + (entry) => entry && typeof entry === "object" && "id" in entry + ) + ) { + normalized.sort((left, right) => + String((left as { id: string }).id).localeCompare( + String((right as { id: string }).id) + ) + ); + } + return normalized; + } + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value as Record).map(([key, entry]) => [ + key, + normalizeValue(entry), + ]) + ); + } + return value; +} + +export function normalizeSnapshot(snapshot: EntitySnapshot): EntitySnapshot { + return normalizeValue(snapshot) as EntitySnapshot; +} diff --git a/subgraphs/launchpad/e2e/run.ts b/subgraphs/launchpad/e2e/run.ts new file mode 100644 index 00000000..ca8c607c --- /dev/null +++ b/subgraphs/launchpad/e2e/run.ts @@ -0,0 +1,1081 @@ +import assert from "node:assert/strict"; +import { spawn, type ChildProcess } from "node:child_process"; +import { createWriteStream } from "node:fs"; +import { mkdir, readFile, realpath, rm, writeFile } from "node:fs/promises"; +import net from "node:net"; +import path from "node:path"; +import { + Contract, + ContractFactory, + Interface, + JsonRpcProvider, + type Signer, + type TransactionReceipt, + type TransactionResponse, +} from "ethers"; +import { + buildExpectedSnapshot, + normalizeSnapshot, + type EntitySnapshot, + type IndexedEvent, + type PoolConfiguration, +} from "./model"; +import { + CHAIN_ID, + DEFAULT_SEEDS, + planScenario, + stringifyPlan, + type LaunchPlan, + type ScenarioAction, + type ScenarioPlan, +} from "./scenario"; + +const RPC_URL = "http://127.0.0.1:8545"; +const GRAPH_ADMIN_URL = "http://127.0.0.1:8020"; +const GRAPH_QUERY_URL = "http://127.0.0.1:8000"; +const IPFS_URL = "http://127.0.0.1:5001"; +const COMPOSE_PROJECT = "launchpad-e2e"; +const REQUIRED_PORTS = [8545, 8000, 8001, 8020, 8030, 8040, 5001, 5432]; + +const packageDirectory = path.resolve(__dirname, ".."); +const repositoryDirectory = path.resolve(packageDirectory, "../.."); +const composeFile = path.join(packageDirectory, "e2e/docker-compose.yml"); +const hardhatConfigSource = path.join( + packageDirectory, + "e2e/hardhat.config.ts" +); + +interface HardhatArtifact { + abi: Array>; + bytecode: string; +} + +interface ContractArtifacts { + launchpad: HardhatArtifact; + launchToken: HardhatArtifact; + weth: HardhatArtifact; + factory: HardhatArtifact; + positionManager: HardhatArtifact; + pool: HardhatArtifact; +} + +interface Fixture { + launchpad: Contract; + launchpadAddress: string; + launchpadDeploymentBlock: number; + weth: Contract; + wethAddress: string; + factory: Contract; + positionManager: Contract; + positionManagerAddress: string; + poolArtifact: HardhatArtifact; + tokenArtifact: HardhatArtifact; +} + +interface LaunchedToken { + tokenKey: string; + address: string; + pool: string; + positionIds: bigint[]; + token: Contract; +} + +interface ScenarioExecution { + events: IndexedEvent[]; + pools: Map; + finalBlock: number; +} + +interface CliOptions { + seeds: number[]; + contractsDirectory: string; +} + +interface RunningProcess { + child: ChildProcess; + finished: Promise; +} + +interface ComposeCli { + command: string; + prefix: string[]; +} + +let composeCli: ComposeCli; + +function parseOptions(): CliOptions { + const arguments_ = process.argv.slice(2); + let seed: number | undefined; + let contractsDirectory = + process.env.LAUNCHPAD_CONTRACTS_DIR ?? + path.resolve(packageDirectory, "../../../sushi-launchpad"); + + for (let index = 0; index < arguments_.length; index += 1) { + const argument = arguments_[index]; + if (argument === "--") continue; + if (argument === "--seed") { + const rawSeed = arguments_[index + 1]; + if (!rawSeed) throw new Error("--seed requires a value"); + seed = Number(rawSeed); + index += 1; + continue; + } + if (argument === "--contracts-dir") { + const rawDirectory = arguments_[index + 1]; + if (!rawDirectory) throw new Error("--contracts-dir requires a value"); + contractsDirectory = rawDirectory; + index += 1; + continue; + } + throw new Error(`Unknown argument: ${argument}`); + } + + if (seed !== undefined) planScenario(seed); + return { + seeds: seed === undefined ? [...DEFAULT_SEEDS] : [seed], + contractsDirectory: path.resolve(contractsDirectory), + }; +} + +function timestampForPath(): string { + return new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-"); +} + +async function runCommand( + command: string, + arguments_: string[], + options: { + cwd: string; + env?: NodeJS.ProcessEnv; + quiet?: boolean; + } +): Promise { + return await new Promise((resolve, reject) => { + const child = spawn(command, arguments_, { + cwd: options.cwd, + env: options.env ?? process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + let output = ""; + child.stdout.on("data", (chunk: Buffer) => { + const text = chunk.toString(); + output += text; + if (!options.quiet) process.stdout.write(text); + }); + child.stderr.on("data", (chunk: Buffer) => { + const text = chunk.toString(); + output += text; + if (!options.quiet) process.stderr.write(text); + }); + child.once("error", reject); + child.once("close", (code) => { + if (code === 0) resolve(output); + else + reject( + new Error( + `${command} ${arguments_.join( + " " + )} exited with code ${code}\n${output}` + ) + ); + }); + }); +} + +function startProcess( + command: string, + arguments_: string[], + cwd: string, + logPath: string +): RunningProcess { + const log = createWriteStream(logPath, { flags: "a" }); + const child = spawn(command, arguments_, { + cwd, + env: process.env, + stdio: ["ignore", "pipe", "pipe"], + }); + child.stdout.pipe(log); + child.stderr.pipe(log); + const finished = new Promise((resolve, reject) => { + child.once("error", reject); + child.once("close", (code) => { + log.end(); + resolve(code); + }); + }); + return { child, finished }; +} + +async function stopProcess( + process_: RunningProcess | undefined +): Promise { + if (!process_ || process_.child.exitCode !== null) return; + process_.child.kill("SIGTERM"); + const stopped = await Promise.race([ + process_.finished.then(() => true), + new Promise((resolve) => setTimeout(() => resolve(false), 5_000)), + ]); + if (!stopped && process_.child.exitCode === null) { + process_.child.kill("SIGKILL"); + await process_.finished; + } +} + +async function portIsAvailable(port: number): Promise { + return await new Promise((resolve, reject) => { + const server = net.createServer(); + server.once("error", (error: NodeJS.ErrnoException) => { + if (error.code === "EADDRINUSE") resolve(false); + else reject(error); + }); + server.listen(port, "127.0.0.1", () => { + server.close((error) => (error ? reject(error) : resolve(true))); + }); + }); +} + +async function assertPortsAvailable(): Promise { + const occupied: number[] = []; + for (const port of REQUIRED_PORTS) { + if (!(await portIsAvailable(port))) occupied.push(port); + } + if (occupied.length > 0) { + throw new Error( + `E2E ports already in use: ${occupied.join( + ", " + )}. Stop the conflicting services and retry.` + ); + } +} + +async function detectComposeCli(): Promise { + try { + await runCommand("docker", ["compose", "version"], { + cwd: repositoryDirectory, + quiet: true, + }); + return { command: "docker", prefix: ["compose"] }; + } catch { + try { + await runCommand("docker-compose", ["version"], { + cwd: repositoryDirectory, + quiet: true, + }); + return { command: "docker-compose", prefix: [] }; + } catch { + throw new Error( + "Docker Compose is unavailable. Install either the Docker Compose plugin or docker-compose." + ); + } + } +} + +function composeArguments(...arguments_: string[]): string[] { + return [ + ...composeCli.prefix, + "-f", + composeFile, + "-p", + COMPOSE_PROJECT, + ...arguments_, + ]; +} + +async function waitUntil( + description: string, + timeoutMs: number, + check: () => Promise +): Promise { + const deadline = Date.now() + timeoutMs; + let lastError: unknown; + while (Date.now() < deadline) { + try { + if (await check()) return; + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + const detail = lastError instanceof Error ? `: ${lastError.message}` : ""; + throw new Error(`Timed out waiting for ${description}${detail}`); +} + +async function waitForHardhat( + provider: JsonRpcProvider, + process_: RunningProcess +): Promise { + const deadline = Date.now() + 60_000; + let lastError: unknown; + while (Date.now() < deadline) { + if (process_.child.exitCode !== null) { + throw new Error( + `Hardhat exited before its RPC became ready (exit code ${process_.child.exitCode}); see hardhat.log` + ); + } + try { + const network = await provider.getNetwork(); + if (network.chainId === CHAIN_ID) return; + } catch (error) { + lastError = error; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + const detail = lastError instanceof Error ? `: ${lastError.message}` : ""; + throw new Error(`Timed out waiting for Hardhat JSON-RPC${detail}`); +} + +async function waitForGraphNode(): Promise { + await waitUntil("Graph Node admin API", 120_000, async () => { + const response = await fetch(GRAPH_ADMIN_URL); + return response.status < 500; + }); + await waitUntil("IPFS API", 60_000, async () => { + const response = await fetch(`${IPFS_URL}/api/v0/id`, { method: "POST" }); + return response.ok; + }); +} + +async function readArtifact(filePath: string): Promise { + const parsed = JSON.parse( + await readFile(filePath, "utf8") + ) as HardhatArtifact; + assert(Array.isArray(parsed.abi), `Artifact has no ABI: ${filePath}`); + assert( + parsed.bytecode?.startsWith("0x"), + `Artifact has no bytecode: ${filePath}` + ); + return parsed; +} + +async function loadArtifacts( + contractsDirectory: string +): Promise { + const artifact = (...segments: string[]): string => + path.join(contractsDirectory, "artifacts/contracts", ...segments); + return { + launchpad: await readArtifact( + artifact("SushiLaunchpad.sol", "SushiLaunchpad.json") + ), + launchToken: await readArtifact( + artifact("SushiLaunchpadToken.sol", "SushiLaunchpadToken.json") + ), + weth: await readArtifact( + artifact("test/MockSushiV3.sol", "MockERC20.json") + ), + factory: await readArtifact( + artifact("test/MockSushiV3.sol", "MockSushiV3Factory.json") + ), + positionManager: await readArtifact( + artifact("test/MockSushiV3.sol", "MockPositionManager.json") + ), + pool: await readArtifact( + artifact("test/MockSushiV3.sol", "MockSushiV3Pool.json") + ), + }; +} + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +async function assertAbiMatches(artifact: HardhatArtifact): Promise { + const pinned = JSON.parse( + await readFile( + path.join(packageDirectory, "abis/SushiLaunchpad.json"), + "utf8" + ) + ) as unknown; + if (canonicalJson(pinned) !== canonicalJson(artifact.abi)) { + throw new Error( + "The compiled SushiLaunchpad ABI differs from abis/SushiLaunchpad.json. Refresh and review the subgraph ABI before running E2E." + ); + } +} + +async function deploy( + artifact: HardhatArtifact, + signer: Signer, + arguments_: unknown[] = [] +): Promise { + const contract = await new ContractFactory( + artifact.abi, + artifact.bytecode, + signer + ).deploy(...arguments_); + await contract.waitForDeployment(); + return contract as unknown as Contract; +} + +async function deployFixture( + provider: JsonRpcProvider, + signers: Signer[], + artifacts: ContractArtifacts, + plan: ScenarioPlan +): Promise { + const owner = signers[plan.actors.owner]!; + const recipient = signers[plan.actors.protocolRecipients[0]]!; + const wethTemplate = await deploy(artifacts.weth, owner, [ + "Wrapped Ether", + "WETH", + 18, + ]); + const runtimeCode = await provider.getCode(await wethTemplate.getAddress()); + await provider.send("hardhat_setCode", [plan.wethAddress, runtimeCode]); + const weth = new Contract(plan.wethAddress, artifacts.weth.abi, owner); + const factory = await deploy(artifacts.factory, owner); + const positionManager = await deploy(artifacts.positionManager, owner, [ + await factory.getAddress(), + plan.wethAddress, + ]); + await (await positionManager.setConsumptionBps(plan.consumptionBps)).wait(); + const launchpad = await deploy(artifacts.launchpad, owner, [ + await owner.getAddress(), + await recipient.getAddress(), + await factory.getAddress(), + await positionManager.getAddress(), + ]); + const deploymentReceipt = await launchpad.deploymentTransaction()!.wait(); + assert(deploymentReceipt, "Launchpad deployment did not produce a receipt"); + + return { + launchpad, + launchpadAddress: (await launchpad.getAddress()).toLowerCase(), + launchpadDeploymentBlock: deploymentReceipt.blockNumber, + weth, + wethAddress: plan.wethAddress.toLowerCase(), + factory, + positionManager, + positionManagerAddress: (await positionManager.getAddress()).toLowerCase(), + poolArtifact: artifacts.pool, + tokenArtifact: artifacts.launchToken, + }; +} + +async function renderManifest( + fixture: Fixture, + seedDirectory: string +): Promise { + const configurationPath = path.join(seedDirectory, "manifest.json"); + await writeFile( + configurationPath, + `${JSON.stringify( + { + launchpad: { + deployments: [ + { + name: "SushiLaunchpad", + network: "hardhat", + chainId: CHAIN_ID.toString(), + address: fixture.launchpadAddress, + startBlock: fixture.launchpadDeploymentBlock, + }, + ], + }, + }, + null, + 2 + )}\n` + ); + const manifest = await runCommand( + "pnpm", + ["exec", "mustache", configurationPath, "template.yaml"], + { cwd: packageDirectory, quiet: true } + ); + await writeFile(path.join(packageDirectory, "subgraph.yaml"), manifest); +} + +async function deploySubgraph(seed: number): Promise { + const name = `sushiswap/launchpad-e2e-${seed}`; + await runCommand("pnpm", ["exec", "graph", "codegen", "subgraph.yaml"], { + cwd: packageDirectory, + }); + await runCommand("pnpm", ["exec", "graph", "build", "subgraph.yaml"], { + cwd: packageDirectory, + }); + await runCommand( + "pnpm", + ["exec", "graph", "create", "--node", GRAPH_ADMIN_URL, name], + { cwd: packageDirectory } + ); + await runCommand( + "pnpm", + [ + "exec", + "graph", + "deploy", + "--node", + GRAPH_ADMIN_URL, + "--ipfs", + IPFS_URL, + "--version-label", + `seed-${seed}`, + name, + "subgraph.yaml", + ], + { cwd: packageDirectory } + ); + return name; +} + +function eventArguments( + parsed: ReturnType +): Record { + assert(parsed, "Expected a parsed Launchpad event"); + return Object.fromEntries( + parsed.fragment.inputs.map((input, index) => [ + input.name, + parsed.args[index], + ]) + ); +} + +async function executeScenario( + provider: JsonRpcProvider, + signers: Signer[], + fixture: Fixture, + plan: ScenarioPlan +): Promise { + const events: IndexedEvent[] = []; + const tokens = new Map(); + const blockTimestamps = new Map(); + const launchpadInterface = new Interface( + fixture.launchpad.interface.fragments + ); + + const recordReceipt = async ( + transaction: TransactionResponse + ): Promise => { + const receipt = await transaction.wait(); + assert(receipt, `Transaction ${transaction.hash} has no receipt`); + let timestamp = blockTimestamps.get(receipt.blockNumber); + if (timestamp === undefined) { + const block = await provider.getBlock(receipt.blockNumber); + assert(block, `Missing block ${receipt.blockNumber}`); + timestamp = block.timestamp; + blockTimestamps.set(receipt.blockNumber, timestamp); + } + + for (const log of receipt.logs) { + if (log.address.toLowerCase() !== fixture.launchpadAddress) continue; + const parsed = launchpadInterface.parseLog({ + topics: [...log.topics], + data: log.data, + }); + if (!parsed) continue; + events.push({ + name: parsed.name, + args: eventArguments(parsed), + transactionHash: receipt.hash.toLowerCase(), + logIndex: log.index, + blockNumber: receipt.blockNumber, + blockHash: receipt.blockHash.toLowerCase(), + timestamp, + }); + } + return receipt; + }; + + const sendLaunch = async ( + action: LaunchPlan + ): Promise => { + const block = await provider.getBlock("latest"); + assert(block, "Hardhat returned no latest block"); + const currentLaunchFee = BigInt(await fixture.launchpad.launchFee()); + assert.equal( + currentLaunchFee, + action.launchFee, + `Planned launch fee drifted for ${action.tokenKey}` + ); + const connected = fixture.launchpad.connect( + signers[action.creatorAccount]! + ) as Contract; + return await connected.launch( + { name: action.name, symbol: action.symbol }, + action.ranges, + BigInt(block.timestamp + 3_600), + { value: action.launchFee } + ); + }; + + const finalizeLaunch = async ( + action: LaunchPlan, + transaction: TransactionResponse + ): Promise => { + const receipt = await recordReceipt(transaction); + const launchEvents = events.filter( + (event) => + event.transactionHash === receipt.hash.toLowerCase() && + event.name === "TokenLaunched" + ); + assert.equal( + launchEvents.length, + 1, + `Missing TokenLaunched for ${action.tokenKey}` + ); + const launchEvent = launchEvents[0]!; + const positionEvents = events.filter( + (event) => + event.transactionHash === receipt.hash.toLowerCase() && + event.name === "PositionCreated" + ); + assert.equal( + positionEvents.length, + action.ranges.length, + `Unexpected position count for ${action.tokenKey}` + ); + const address = String(launchEvent.args.token).toLowerCase(); + tokens.set(action.tokenKey, { + tokenKey: action.tokenKey, + address, + pool: String(launchEvent.args.pool).toLowerCase(), + positionIds: positionEvents.map((event) => + BigInt(event.args.positionId as bigint) + ), + token: new Contract(address, fixture.tokenArtifact.abi, signers[0]), + }); + }; + + const transact = async ( + action: Exclude + ) => { + if (action.kind === "launch") { + const transaction = await sendLaunch(action); + await finalizeLaunch(action, transaction); + return; + } + if (action.kind === "setDefaultSushiFeeBps") { + await recordReceipt( + await fixture.launchpad.setDefaultSushiFeeBps(action.value) + ); + return; + } + if (action.kind === "setProtocolReserveBps") { + await recordReceipt( + await fixture.launchpad.setProtocolReserveBps(action.value) + ); + return; + } + if (action.kind === "setProtocolRecipient") { + await recordReceipt( + await fixture.launchpad.setProtocolRecipient( + await signers[action.account]!.getAddress() + ) + ); + return; + } + if (action.kind === "setLaunchFee") { + await recordReceipt(await fixture.launchpad.setLaunchFee(action.value)); + return; + } + if (action.kind === "setTokenSushiFeeBps") { + const token = tokens.get(action.tokenKey); + assert(token, `Unknown planned token ${action.tokenKey}`); + await recordReceipt( + await fixture.launchpad.setSushiFeeBps(token.address, action.value) + ); + return; + } + if (action.kind === "distributeFees") { + const token = tokens.get(action.tokenKey); + assert(token, `Unknown planned token ${action.tokenKey}`); + const pool = new Contract( + token.pool, + fixture.poolArtifact.abi, + signers[0] + ); + const tokenIs0 = + String(await pool.token0()).toLowerCase() === + token.address.toLowerCase(); + await ( + await fixture.weth.mint( + fixture.positionManagerAddress, + action.wethAmount + ) + ).wait(); + await ( + await fixture.positionManager.seedFees( + token.positionIds[action.positionIndex], + tokenIs0 ? action.tokenAmount : action.wethAmount, + tokenIs0 ? action.wethAmount : action.tokenAmount + ) + ).wait(); + const connected = fixture.launchpad.connect( + signers[action.callerAccount]! + ) as Contract; + await recordReceipt(await connected.distributeFees(token.address)); + return; + } + if (action.kind === "withdrawLaunchFees") { + await recordReceipt(await fixture.launchpad.withdrawLaunchFees()); + return; + } + if (action.kind === "advanceTime") { + await provider.send("evm_increaseTime", [action.seconds]); + await provider.send("evm_mine", []); + return; + } + if (action.kind === "withdrawReserve") { + const token = tokens.get(action.tokenKey); + assert(token, `Unknown planned token ${action.tokenKey}`); + await recordReceipt( + await fixture.launchpad.withdrawProtocolReserve(token.address) + ); + return; + } + const exhaustive: never = action; + throw new Error(`Unhandled action ${(exhaustive as ScenarioAction).kind}`); + }; + + for (const action of plan.actions) { + if (action.kind !== "sameBlock") { + await transact(action); + continue; + } + + const [launch, defaultUpdate, reserveUpdate] = action.actions; + await provider.send("evm_setAutomine", [false]); + try { + const launchTransaction = await sendLaunch(launch); + const ownerAddress = await signers[0]!.getAddress(); + const ownerNonce = await provider.getTransactionCount( + ownerAddress, + "pending" + ); + const defaultTransaction = await fixture.launchpad.setDefaultSushiFeeBps( + defaultUpdate.value, + { nonce: ownerNonce } + ); + const reserveTransaction = await fixture.launchpad.setProtocolReserveBps( + reserveUpdate.value, + { nonce: ownerNonce + 1 } + ); + await provider.send("evm_mine", []); + await finalizeLaunch(launch, launchTransaction); + await recordReceipt(defaultTransaction); + await recordReceipt(reserveTransaction); + } finally { + await provider.send("evm_setAutomine", [true]); + } + } + + const pools = new Map(); + for (const token of tokens.values()) { + if (pools.has(token.pool)) continue; + const contract = new Contract( + token.pool, + fixture.poolArtifact.abi, + signers[0] + ); + pools.set(token.pool, { + address: token.pool, + token0: String(await contract.token0()).toLowerCase(), + token1: String(await contract.token1()).toLowerCase(), + fee: Number(await contract.fee()), + tickSpacing: Number(await contract.tickSpacing()), + }); + } + + return { + events, + pools, + finalBlock: await provider.getBlockNumber(), + }; +} + +async function graphQl(endpoint: string, query: string): Promise { + const response = await fetch(endpoint, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ query }), + }); + if (!response.ok) { + throw new Error( + `GraphQL HTTP ${response.status}: ${await response.text()}` + ); + } + const payload = (await response.json()) as { + data?: T; + errors?: Array<{ message: string }>; + }; + if (payload.errors?.length || !payload.data) { + throw new Error( + `GraphQL query failed: ${ + payload.errors?.map((error) => error.message).join("; ") ?? "no data" + }` + ); + } + return payload.data; +} + +async function waitForIndexedHead( + endpoint: string, + finalBlock: number +): Promise { + await waitUntil(`subgraph head block ${finalBlock}`, 120_000, async () => { + const data = await graphQl<{ + _meta: { block: { number: number }; hasIndexingErrors: boolean } | null; + }>( + endpoint, + "query E2EHead { _meta { block { number } hasIndexingErrors } }" + ); + if (data._meta?.hasIndexingErrors) { + throw new Error("Subgraph reports indexing errors"); + } + return (data._meta?.block.number ?? -1) >= finalBlock; + }); +} + +const snapshotQuery = ` + query E2ESnapshot { + launchpads(first: 1000, orderBy: id) { + id chainId address quoteToken positionManager protocolRecipient launchFee + defaultSushiFeeBps protocolReserveBps tokenCount creatorCount positionCount + tokens { id } pools { id } creators { id } launchFeeWithdrawals { id } + } + creators(first: 1000, orderBy: id) { + id chainId address launchpad { id } tokenCount tokens { id } + } + tokens(first: 1000, orderBy: id) { + id chainId address launchpad { id } creator { id } pool { id } + name symbol decimals totalSupply sushiFeeBps reserveBps reserveAmount + reserveUnlockAt reserveWithdrawn reserveWithdrawal { id } + totalAmount0Collected totalAmount1Collected totalAmount0ToSushi + totalAmount1ToSushi totalAmount0ToCreator totalAmount1ToCreator + feeDistributions { id } + creationTransactionHash creationLogIndex creationBlockNumber + creationBlockHash createdAt + } + pools(first: 1000, orderBy: id) { + id chainId address launchpad { id } positions { id } + token0 token1 fee tickSpacing positionManager positionCount + creationTransactionHash creationBlockNumber creationBlockHash createdAt + } + launchPositions(first: 1000, orderBy: id) { + id chainId positionManager positionId index pool { id } + tickLower tickUpper liquidity amount0Desired amount1Desired amount0 amount1 + creationTransactionHash creationLogIndex creationBlockNumber + creationBlockHash createdAt + } + feeDistributions(first: 1000, orderBy: id) { + id chainId token { id } pool { id } caller sushiRecipient creatorRecipient + sushiFeeBps amount0Collected amount1Collected amount0ToSushi amount1ToSushi + amount0ToCreator amount1ToCreator transactionHash logIndex blockNumber + blockHash timestamp + } + reserveWithdrawals(first: 1000, orderBy: id) { + id chainId token { id } recipient amount transactionHash logIndex + blockNumber blockHash timestamp + } + launchFeeWithdrawals(first: 1000, orderBy: id) { + id chainId launchpad { id } recipient amount transactionHash logIndex + blockNumber blockHash timestamp + } + } +`; + +async function runSeed( + seed: number, + provider: JsonRpcProvider, + signers: Signer[], + artifacts: ContractArtifacts, + runDirectory: string +): Promise { + const seedDirectory = path.join(runDirectory, `seed-${seed}`); + await mkdir(seedDirectory, { recursive: true }); + const plan = planScenario(seed); + await writeFile(path.join(seedDirectory, "plan.json"), stringifyPlan(plan)); + process.stdout.write(`\nSeed ${seed}: deploying fixture\n`); + const fixture = await deployFixture(provider, signers, artifacts, plan); + await renderManifest(fixture, seedDirectory); + const subgraphName = await deploySubgraph(seed); + const endpoint = `${GRAPH_QUERY_URL}/subgraphs/name/${subgraphName}`; + + process.stdout.write( + `Seed ${seed}: executing ${plan.actions.length} actions\n` + ); + const execution = await executeScenario(provider, signers, fixture, plan); + await writeFile( + path.join(seedDirectory, "trace.json"), + `${JSON.stringify( + execution.events, + (_key, value: unknown) => + typeof value === "bigint" ? value.toString() : value, + 2 + )}\n` + ); + await waitForIndexedHead(endpoint, execution.finalBlock); + + const actual = normalizeSnapshot( + await graphQl(endpoint, snapshotQuery) + ); + const expected = buildExpectedSnapshot({ + chainId: CHAIN_ID, + launchpadAddress: fixture.launchpadAddress, + quoteToken: fixture.wethAddress, + positionManager: fixture.positionManagerAddress, + protocolRecipient: String(await fixture.launchpad.protocolRecipient()), + launchFee: BigInt(await fixture.launchpad.launchFee()), + defaultSushiFeeBps: Number(await fixture.launchpad.defaultSushiFeeBps()), + protocolReserveBps: Number(await fixture.launchpad.protocolReserveBps()), + events: execution.events, + pools: execution.pools, + }); + await writeFile( + path.join(seedDirectory, "graphql.json"), + `${JSON.stringify(actual, null, 2)}\n` + ); + await writeFile( + path.join(seedDirectory, "expected.json"), + `${JSON.stringify(expected, null, 2)}\n` + ); + assert.deepStrictEqual( + actual, + expected, + `Subgraph projection mismatch for seed ${seed}` + ); + process.stdout.write(`Seed ${seed}: projection verified\n`); +} + +async function captureComposeLogs(runDirectory: string): Promise { + try { + const logs = await runCommand( + composeCli.command, + composeArguments("logs", "--no-color"), + { cwd: repositoryDirectory, quiet: true } + ); + await writeFile(path.join(runDirectory, "docker-compose.log"), logs); + } catch (error) { + await writeFile( + path.join(runDirectory, "docker-compose.log"), + `${error instanceof Error ? error.stack : String(error)}\n` + ); + } +} + +async function composeDown(): Promise { + await runCommand( + composeCli.command, + composeArguments("down", "--volumes", "--remove-orphans"), + { cwd: repositoryDirectory, quiet: true } + ).catch(() => undefined); +} + +async function main(): Promise { + const options = parseOptions(); + options.contractsDirectory = await realpath(options.contractsDirectory).catch( + () => { + throw new Error( + `Sushi Launchpad checkout not found at ${options.contractsDirectory}. Set LAUNCHPAD_CONTRACTS_DIR or pass --contracts-dir.` + ); + } + ); + const runDirectory = path.join( + packageDirectory, + ".e2e-artifacts", + timestampForPath() + ); + await mkdir(runDirectory, { recursive: true }); + composeCli = await detectComposeCli(); + await assertPortsAvailable(); + + process.stdout.write( + "Compiling the configured Launchpad contract checkout\n" + ); + await runCommand("pnpm", ["compile"], { + cwd: options.contractsDirectory, + }); + const artifacts = await loadArtifacts(options.contractsDirectory); + await assertAbiMatches(artifacts.launchpad); + + let hardhat: RunningProcess | undefined; + let composeStarted = false; + let cleaningUp = false; + const runtimeHardhatConfig = path.join( + options.contractsDirectory, + "cache/launchpad-e2e.config.ts" + ); + await mkdir(path.dirname(runtimeHardhatConfig), { recursive: true }); + await writeFile( + runtimeHardhatConfig, + await readFile(hardhatConfigSource, "utf8") + ); + const cleanup = async (): Promise => { + if (cleaningUp) return; + cleaningUp = true; + if (composeStarted) await captureComposeLogs(runDirectory); + if (composeStarted) await composeDown(); + await stopProcess(hardhat); + await rm(runtimeHardhatConfig, { force: true }); + }; + const interrupt = (signal: NodeJS.Signals) => { + void cleanup().finally(() => { + process.kill(process.pid, signal); + }); + }; + process.once("SIGINT", interrupt); + process.once("SIGTERM", interrupt); + + try { + hardhat = startProcess( + "pnpm", + [ + "exec", + "hardhat", + "--config", + runtimeHardhatConfig, + "--network", + "hardhatMainnet", + "node", + "--hostname", + "0.0.0.0", + ], + options.contractsDirectory, + path.join(runDirectory, "hardhat.log") + ); + const provider = new JsonRpcProvider(RPC_URL); + await waitForHardhat(provider, hardhat); + + composeStarted = true; + await runCommand(composeCli.command, composeArguments("up", "-d"), { + cwd: repositoryDirectory, + }); + await waitForGraphNode(); + + const signers: Signer[] = []; + for (let index = 0; index < 8; index += 1) { + signers.push(await provider.getSigner(index)); + } + for (const seed of options.seeds) { + await runSeed(seed, provider, signers, artifacts, runDirectory); + } + + process.stdout.write( + `\nAll ${options.seeds.length} E2E seed(s) passed. Artifacts: ${runDirectory}\n` + ); + } catch (error) { + process.stderr.write( + `\nE2E failed. Diagnostics: ${runDirectory}\n${ + error instanceof Error ? error.stack : String(error) + }\n` + ); + process.exitCode = 1; + } finally { + process.removeListener("SIGINT", interrupt); + process.removeListener("SIGTERM", interrupt); + await cleanup(); + } +} + +void main().catch((error: unknown) => { + process.stderr.write( + `${error instanceof Error ? error.stack : String(error)}\n` + ); + process.exitCode = 1; +}); diff --git a/subgraphs/launchpad/e2e/scenario.test.ts b/subgraphs/launchpad/e2e/scenario.test.ts new file mode 100644 index 00000000..b6b786dc --- /dev/null +++ b/subgraphs/launchpad/e2e/scenario.test.ts @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + DEFAULT_SEEDS, + planScenario, + stringifyPlan, + validateScenario, +} from "./scenario"; + +test("the same seed always produces the same complete plan", () => { + const first = stringifyPlan(planScenario(202)); + const second = stringifyPlan(planScenario(202)); + + assert.equal(first, second); +}); + +test("different seeds vary the planned real-world behavior", () => { + const plans = DEFAULT_SEEDS.map((seed) => stringifyPlan(planScenario(seed))); + + assert.equal(new Set(plans).size, DEFAULT_SEEDS.length); +}); + +test("the default matrix satisfies all scenario invariants", () => { + for (const seed of DEFAULT_SEEDS) { + const plan = planScenario(seed); + assert.doesNotThrow(() => validateScenario(plan)); + } +}); + +test("invalid seeds fail before any chain process starts", () => { + assert.throws(() => planScenario(-1), /unsigned 32-bit integer/); + assert.throws(() => planScenario(2 ** 32), /unsigned 32-bit integer/); + assert.throws(() => planScenario(1.5), /unsigned 32-bit integer/); +}); diff --git a/subgraphs/launchpad/e2e/scenario.ts b/subgraphs/launchpad/e2e/scenario.ts new file mode 100644 index 00000000..f6a32dc3 --- /dev/null +++ b/subgraphs/launchpad/e2e/scenario.ts @@ -0,0 +1,343 @@ +export const DEFAULT_SEEDS = [101, 202, 303] as const; + +export const CHAIN_ID = 31_337n; +export const UNIT = 10n ** 18n; +export const TOKEN_SUPPLY = 1_000_000_000n * UNIT; +export const INITIAL_LAUNCH_FEE = 500_000_000_000_000n; +export const INITIAL_SUSHI_FEE_BPS = 7_000; +export const INITIAL_RESERVE_BPS = 300; +export const RESERVE_LOCK_SECONDS = 365 * 24 * 60 * 60; + +export const LOW_WETH_ADDRESS = "0x0000000000000000000000000000000000001000"; +export const HIGH_WETH_ADDRESS = "0xfffffffffffffffffffffffffffffffffffffffe"; + +export interface SaleRangePlan { + startTick: number; + endTick: number; + amount: bigint; +} + +export interface LaunchPlan { + kind: "launch"; + tokenKey: string; + creatorAccount: number; + name: string; + symbol: string; + ranges: SaleRangePlan[]; + launchFee: bigint; +} + +export type ScenarioAction = + | LaunchPlan + | { + kind: "sameBlock"; + actions: [ + LaunchPlan, + { kind: "setDefaultSushiFeeBps"; value: number }, + { kind: "setProtocolReserveBps"; value: number } + ]; + } + | { kind: "setDefaultSushiFeeBps"; value: number } + | { kind: "setProtocolReserveBps"; value: number } + | { kind: "setProtocolRecipient"; account: number } + | { kind: "setLaunchFee"; value: bigint } + | { kind: "setTokenSushiFeeBps"; tokenKey: string; value: number } + | { + kind: "distributeFees"; + tokenKey: string; + callerAccount: number; + positionIndex: number; + wethAmount: bigint; + tokenAmount: bigint; + } + | { kind: "withdrawLaunchFees" } + | { kind: "advanceTime"; seconds: number } + | { kind: "withdrawReserve"; tokenKey: string }; + +export interface ScenarioPlan { + seed: number; + wethAddress: string; + consumptionBps: number; + actors: { + owner: 0; + protocolRecipients: [1, 2]; + creators: [3, 4, 5]; + callers: [6, 7]; + }; + actions: ScenarioAction[]; + intentionallyUnwithdrawnToken: string; +} + +class SeededRandom { + private state: number; + + constructor(seed: number) { + this.state = seed >>> 0; + } + + next(): number { + this.state = (this.state + 0x6d2b79f5) >>> 0; + let value = this.state; + value = Math.imul(value ^ (value >>> 15), value | 1); + value ^= value + Math.imul(value ^ (value >>> 7), value | 61); + return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296; + } + + int(min: number, max: number): number { + return min + Math.floor(this.next() * (max - min + 1)); + } + + pick(values: readonly T[]): T { + return values[this.int(0, values.length - 1)]!; + } + + shuffle(values: readonly T[]): T[] { + const shuffled = [...values]; + for (let index = shuffled.length - 1; index > 0; index -= 1) { + const other = this.int(0, index); + [shuffled[index], shuffled[other]] = [shuffled[other]!, shuffled[index]!]; + } + return shuffled; + } +} + +function launchPlan( + random: SeededRandom, + index: number, + creatorAccount: number +): LaunchPlan { + const rangeCount = random.int(1, 4); + const firstTick = -rangeCount * 200; + const ranges: SaleRangePlan[] = []; + + for (let rangeIndex = 0; rangeIndex < rangeCount; rangeIndex += 1) { + ranges.push({ + startTick: firstTick + rangeIndex * 200, + endTick: firstTick + (rangeIndex + 1) * 200, + amount: BigInt(random.int(80, 140)) * 1_000_000n * UNIT, + }); + } + + return { + kind: "launch", + tokenKey: `token-${index}`, + creatorAccount, + name: `Launchpad Agent ${index + 1}`, + symbol: `LPA${index + 1}`, + ranges, + launchFee: INITIAL_LAUNCH_FEE, + }; +} + +function distribution( + random: SeededRandom, + tokenKey: string, + callerAccount: number, + maxPositionIndex: number +): ScenarioAction { + return { + kind: "distributeFees", + tokenKey, + callerAccount, + positionIndex: random.int(0, maxPositionIndex), + wethAmount: BigInt(random.int(51, 500)), + tokenAmount: BigInt(random.int(51, 500)), + }; +} + +export function planScenario(seed: number): ScenarioPlan { + if (!Number.isInteger(seed) || seed < 0 || seed > 0xffff_ffff) { + throw new Error( + `Seed must be an unsigned 32-bit integer, received ${seed}` + ); + } + + const random = new SeededRandom(seed); + const creators = random.shuffle([3, 3, 4, 5]); + const launches = creators.map((creator, index) => + launchPlan(random, index, creator) + ); + const newDefault = random.pick([1_500, 2_500, 4_500, 6_000, 8_000]); + const newReserve = random.pick([100, 500, 750, 1_000]); + const finalDefault = random.pick( + [1_000, 3_500, 5_500, 9_000].filter((value) => value !== newDefault) + ); + const finalReserve = random.pick( + [0, 200, 600, 900].filter((value) => value !== newReserve) + ); + const newLaunchFee = random.pick([ + 100_000_000_000_000n, + 700_000_000_000_000n, + 1_000_000_000_000_000n, + ]); + + const firstLaunch = launches[0]!; + const remaining = random.shuffle(launches.slice(1)); + const firstRemaining = remaining[0]!; + const secondRemaining = remaining[1]!; + const finalLaunch = remaining[2]!; + + const actions: ScenarioAction[] = [ + { + kind: "sameBlock", + actions: [ + firstLaunch, + { kind: "setDefaultSushiFeeBps", value: newDefault }, + { kind: "setProtocolReserveBps", value: newReserve }, + ], + }, + ]; + + if (random.int(0, 1) === 0) { + actions.push({ kind: "setLaunchFee", value: newLaunchFee }); + firstRemaining.launchFee = newLaunchFee; + secondRemaining.launchFee = newLaunchFee; + finalLaunch.launchFee = newLaunchFee; + actions.push(firstRemaining, secondRemaining); + } else { + actions.push(firstRemaining); + actions.push({ kind: "setLaunchFee", value: newLaunchFee }); + secondRemaining.launchFee = newLaunchFee; + finalLaunch.launchFee = newLaunchFee; + actions.push(secondRemaining); + } + + actions.push({ kind: "withdrawLaunchFees" }); + actions.push(finalLaunch); + actions.push({ kind: "withdrawLaunchFees" }); + actions.push( + { kind: "setDefaultSushiFeeBps", value: finalDefault }, + { kind: "setProtocolReserveBps", value: finalReserve } + ); + + const firstTokenFee = random.pick([1_234, 3_333, 5_000, 8_765]); + const secondTokenFee = random.pick([777, 2_222, 6_666, 9_999]); + actions.push({ + kind: "setTokenSushiFeeBps", + tokenKey: firstLaunch.tokenKey, + value: firstTokenFee, + }); + actions.push( + distribution( + random, + firstLaunch.tokenKey, + random.pick([6, 7]), + firstLaunch.ranges.length - 1 + ) + ); + actions.push({ kind: "setProtocolRecipient", account: 2 }); + actions.push( + distribution( + random, + firstLaunch.tokenKey, + random.pick([6, 7]), + firstLaunch.ranges.length - 1 + ) + ); + actions.push({ + kind: "setTokenSushiFeeBps", + tokenKey: firstRemaining.tokenKey, + value: secondTokenFee, + }); + actions.push( + distribution( + random, + firstRemaining.tokenKey, + random.pick([6, 7]), + firstRemaining.ranges.length - 1 + ) + ); + + const tokenKeys = launches.map((launch) => launch.tokenKey); + const intentionallyUnwithdrawnToken = random.pick(tokenKeys); + const withdrawals = random.shuffle( + tokenKeys.filter((tokenKey) => tokenKey !== intentionallyUnwithdrawnToken) + ); + actions.push({ + kind: "advanceTime", + seconds: RESERVE_LOCK_SECONDS + 100, + }); + for (const tokenKey of withdrawals) { + actions.push({ kind: "withdrawReserve", tokenKey }); + } + + const plan: ScenarioPlan = { + seed, + wethAddress: seed % 2 === 0 ? HIGH_WETH_ADDRESS : LOW_WETH_ADDRESS, + consumptionBps: [10_000, 9_999, 9_750][seed % 3]!, + actors: { + owner: 0, + protocolRecipients: [1, 2], + creators: [3, 4, 5], + callers: [6, 7], + }, + actions, + intentionallyUnwithdrawnToken, + }; + + validateScenario(plan); + return plan; +} + +export function validateScenario(plan: ScenarioPlan): void { + const flattened = plan.actions.flatMap((action) => + action.kind === "sameBlock" ? action.actions : [action] + ); + const launches = flattened.filter( + (action): action is LaunchPlan => action.kind === "launch" + ); + const launchKeys = new Set(launches.map((launch) => launch.tokenKey)); + + if (launches.length !== 4 || launchKeys.size !== 4) { + throw new Error("Every scenario must contain four unique launches"); + } + if (new Set(launches.map((launch) => launch.creatorAccount)).size !== 3) { + throw new Error("Every scenario must contain exactly three creators"); + } + if (!launchKeys.has(plan.intentionallyUnwithdrawnToken)) { + throw new Error("The intentionally unwithdrawn token must be launched"); + } + + for (const launch of launches) { + if (launch.ranges.length < 1 || launch.ranges.length > 16) { + throw new Error(`Invalid range count for ${launch.tokenKey}`); + } + let amountSum = 0n; + for (let index = 0; index < launch.ranges.length; index += 1) { + const range = launch.ranges[index]!; + if ( + range.amount <= 0n || + range.startTick >= range.endTick || + range.startTick % 200 !== 0 || + range.endTick % 200 !== 0 || + (index > 0 && range.startTick !== launch.ranges[index - 1]!.endTick) + ) { + throw new Error(`Invalid range ${index} for ${launch.tokenKey}`); + } + amountSum += range.amount; + } + if (amountSum > (TOKEN_SUPPLY * 9_000n) / 10_000n) { + throw new Error(`Range allocation is unsafe for ${launch.tokenKey}`); + } + } + + const count = (kind: ScenarioAction["kind"]): number => + flattened.filter((action) => action.kind === kind).length; + if ( + count("distributeFees") !== 3 || + count("withdrawLaunchFees") !== 2 || + count("withdrawReserve") !== 3 || + count("setProtocolRecipient") !== 1 + ) { + throw new Error("Scenario is missing required lifecycle coverage"); + } +} + +export function stringifyPlan(plan: ScenarioPlan): string { + return `${JSON.stringify( + plan, + (_key, value: unknown) => + typeof value === "bigint" ? value.toString() : value, + 2 + )}\n`; +} diff --git a/subgraphs/launchpad/e2e/tsconfig.json b/subgraphs/launchpad/e2e/tsconfig.json new file mode 100644 index 00000000..bc03dcc0 --- /dev/null +++ b/subgraphs/launchpad/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "Node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "files": ["scenario.ts", "scenario.test.ts", "model.ts", "run.ts"] +} diff --git a/subgraphs/launchpad/package.json b/subgraphs/launchpad/package.json index 20315766..cccf2bdc 100644 --- a/subgraphs/launchpad/package.json +++ b/subgraphs/launchpad/package.json @@ -11,13 +11,20 @@ "scripts": { "generate": "mustache ../../config/$NETWORK.js template.yaml > subgraph.yaml && graph codegen", "build": "graph build", - "test": "graph test -r" + "test": "graph test -r", + "test:e2e": "tsx e2e/run.ts", + "test:e2e:planner": "tsx --test e2e/scenario.test.ts", + "test:e2e:typecheck": "tsc -p e2e/tsconfig.json" }, "devDependencies": { "@graphprotocol/graph-cli": "^0.96.0", "@graphprotocol/graph-ts": "^0.27.0", + "@types/node": "^22.13.11", "assemblyscript": "^0.19.20", + "ethers": "^6.14.0", "matchstick-as": "0.6.0", + "tsx": "^4.20.3", + "typescript": "^5.8.2", "wabt": "1.0.24" } } From fb5ed084af65983ed508b4e936969f61580004a3 Mon Sep 17 00:00:00 2001 From: LufyCZ Date: Thu, 23 Jul 2026 23:54:52 +0000 Subject: [PATCH 4/7] feat: custom quote token --- subgraphs/launchpad/README.md | 23 ++++---- subgraphs/launchpad/abis/SushiLaunchpad.json | 52 ++++++++++------ subgraphs/launchpad/e2e/model.ts | 40 ++++++++++--- subgraphs/launchpad/e2e/run.ts | 62 +++++++++++++------- subgraphs/launchpad/e2e/scenario.test.ts | 6 ++ subgraphs/launchpad/e2e/scenario.ts | 25 ++++++-- subgraphs/launchpad/schema.graphql | 2 +- subgraphs/launchpad/src/mappings/helpers.ts | 1 - subgraphs/launchpad/src/mappings/pool.ts | 14 ++--- subgraphs/launchpad/src/mappings/token.ts | 23 ++++---- subgraphs/launchpad/template.yaml | 2 +- subgraphs/launchpad/tests/Launchpad.test.ts | 51 +++++++++++++--- subgraphs/launchpad/tests/mocks.ts | 19 ++++-- 13 files changed, 222 insertions(+), 98 deletions(-) diff --git a/subgraphs/launchpad/README.md b/subgraphs/launchpad/README.md index 5dfaa979..5b0e1753 100644 --- a/subgraphs/launchpad/README.md +++ b/subgraphs/launchpad/README.md @@ -11,8 +11,8 @@ data-api market projection. aggregate token, creator, and position counts. - `Creator` provides the per-Launchpad identity needed to count distinct creators and links each creator to their tokens. -- `Token` stores ERC-20 metadata, its creator, current Sushi fee, reserve state, - cumulative fee amounts, and its pool. +- `Token` stores ERC-20 metadata, its creator, per-launch quote token, current + Sushi fee, reserve state, cumulative fee amounts, and its pool. - `Pool` stores the immutable V3 configuration (`token0`, `token1`, fee, tick spacing, and position manager) and links to its initial launch positions. - `LaunchPosition` stores only the V3 NFTs minted in the token launch @@ -51,8 +51,11 @@ launchpad: { } ``` -All deployments on a network share the schema and mappings. The mapping reads -the quote token, position manager, protocol recipient, launch fee, and initial +All deployments on a network share the schema and mappings. A quote token is +selected independently for each launch and persisted on its `Token` entity. +The subgraph indexes every valid quote token emitted by the contract; public +allowlisting and market filtering belong downstream in data-api. The mapping +reads the position manager, protocol recipient, launch fee, and initial defaults from the Launchpad contract, then reads canonical token ordering, fee, and tick spacing from each created V3 pool. Add old and new factory versions to the array during a rotation; do not replace historical entries. Keep the first @@ -77,18 +80,18 @@ pnpm build pnpm test ``` -`abis/SushiLaunchpad.json` is pinned from the current contract artifact in the -`sushi-launchpad` repository. Refresh it together with mappings and tests when -an indexed event changes. +`abis/SushiLaunchpad.json` is pinned from the `SushiLaunchpad` artifact at +contract commit `ee5f8bb`. Refresh it together with mappings and tests when an +indexed event changes. ## Local end-to-end test The E2E harness runs the real Launchpad and launched token contracts against a Hardhat chain, indexes their events with a local Graph Node, and compares every entity with an independently constructed expected projection. The Sushi V3 -factory, pools, position manager, and WETH use the contract repository's -behavioral mocks so fee collection and canonical token ordering remain fast and -deterministic. +factory, pools, position manager, and per-launch quote tokens use the contract +repository's behavioral mocks so fee collection and canonical token ordering +remain fast and deterministic. Prerequisites: diff --git a/subgraphs/launchpad/abis/SushiLaunchpad.json b/subgraphs/launchpad/abis/SushiLaunchpad.json index 950cf13c..c63e3c18 100644 --- a/subgraphs/launchpad/abis/SushiLaunchpad.json +++ b/subgraphs/launchpad/abis/SushiLaunchpad.json @@ -104,6 +104,17 @@ "name": "InvalidPositionManager", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + } + ], + "name": "InvalidQuoteToken", + "type": "error" + }, { "inputs": [ { @@ -234,7 +245,7 @@ "type": "uint256" } ], - "name": "NonzeroWethUsed", + "name": "NonzeroQuoteTokenUsed", "type": "error" }, { @@ -567,7 +578,7 @@ { "indexed": false, "internalType": "uint256", - "name": "wethCollected", + "name": "quoteCollected", "type": "uint256" }, { @@ -579,7 +590,7 @@ { "indexed": false, "internalType": "uint256", - "name": "wethToSushi", + "name": "quoteToSushi", "type": "uint256" }, { @@ -591,7 +602,7 @@ { "indexed": false, "internalType": "uint256", - "name": "wethToCreator", + "name": "quoteToCreator", "type": "uint256" }, { @@ -844,6 +855,12 @@ "name": "pool", "type": "address" }, + { + "indexed": false, + "internalType": "address", + "name": "quoteToken", + "type": "address" + }, { "indexed": false, "internalType": "string", @@ -902,19 +919,6 @@ "name": "TokenLaunched", "type": "event" }, - { - "inputs": [], - "name": "WETH", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "acceptOwnership", @@ -947,7 +951,7 @@ "outputs": [ { "internalType": "uint256", - "name": "wethCollected", + "name": "quoteCollected", "type": "uint256" }, { @@ -957,7 +961,7 @@ }, { "internalType": "uint256", - "name": "wethToSushi", + "name": "quoteToSushi", "type": "uint256" }, { @@ -988,6 +992,11 @@ "name": "tokenConfig", "type": "tuple" }, + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + }, { "components": [ { @@ -1067,6 +1076,11 @@ "name": "creator", "type": "address" }, + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + }, { "internalType": "address", "name": "pool", diff --git a/subgraphs/launchpad/e2e/model.ts b/subgraphs/launchpad/e2e/model.ts index 75236f97..8582e14e 100644 --- a/subgraphs/launchpad/e2e/model.ts +++ b/subgraphs/launchpad/e2e/model.ts @@ -21,7 +21,6 @@ export interface PoolConfiguration { export interface ExpectedModelInput { chainId: bigint; launchpadAddress: string; - quoteToken: string; positionManager: string; protocolRecipient: string; launchFee: bigint; @@ -101,7 +100,6 @@ export function buildExpectedSnapshot( id: launchpadId, chainId: input.chainId.toString(), address: input.launchpadAddress.toLowerCase(), - quoteToken: input.quoteToken.toLowerCase(), positionManager: input.positionManager.toLowerCase(), protocolRecipient: input.protocolRecipient.toLowerCase(), launchFee: input.launchFee.toString(), @@ -134,6 +132,15 @@ export function buildExpectedSnapshot( const idForPool = addressId(input.chainId, poolAddress); let pool = pools.get(idForPool); if (!pool) { + const launchedToken = lower(args.token); + const tokenIs0 = + poolConfiguration.token0.toLowerCase() === launchedToken; + const tokenIs1 = + poolConfiguration.token1.toLowerCase() === launchedToken; + assert( + tokenIs0 !== tokenIs1, + `Launch token is not exactly one side of pool ${poolAddress}` + ); pool = { id: idForPool, chainId: input.chainId.toString(), @@ -196,6 +203,22 @@ export function buildExpectedSnapshot( const idForToken = addressId(input.chainId, args.token); const idForCreator = `${launchpadId}:${lower(args.creator)}`; const idForPool = addressId(input.chainId, args.pool); + const pool = pools.get(idForPool); + assert(pool, `Missing launch pool ${idForPool}`); + const tokenAddress = lower(args.token); + const quoteToken = lower(args.quoteToken); + assert( + (lower(pool.token0) === tokenAddress && + lower(pool.token1) === quoteToken) || + (lower(pool.token1) === tokenAddress && + lower(pool.token0) === quoteToken), + `TokenLaunched pair does not match pool ${idForPool}` + ); + assert.equal( + pool.positionCount, + integer(args.positionCount), + `TokenLaunched position count does not match pool ${idForPool}` + ); let creator = creators.get(idForCreator); if (!creator) { creator = { @@ -219,6 +242,7 @@ export function buildExpectedSnapshot( launchpad: relation(launchpadId), creator: relation(idForCreator), pool: relation(idForPool), + quoteToken, name: String(args.name), symbol: String(args.symbol), decimals: integer(args.decimals), @@ -267,21 +291,21 @@ export function buildExpectedSnapshot( const tokenIs0 = lower(pool.token0) === lower(args.token); const amount0Collected = tokenIs0 ? decimal(args.tokenCollected) - : decimal(args.wethCollected); + : decimal(args.quoteCollected); const amount1Collected = tokenIs0 - ? decimal(args.wethCollected) + ? decimal(args.quoteCollected) : decimal(args.tokenCollected); const amount0ToSushi = tokenIs0 ? decimal(args.tokenToSushi) - : decimal(args.wethToSushi); + : decimal(args.quoteToSushi); const amount1ToSushi = tokenIs0 - ? decimal(args.wethToSushi) + ? decimal(args.quoteToSushi) : decimal(args.tokenToSushi); const amount0ToCreator = tokenIs0 ? decimal(args.tokenToCreator) - : decimal(args.wethToCreator); + : decimal(args.quoteToCreator); const amount1ToCreator = tokenIs0 - ? decimal(args.wethToCreator) + ? decimal(args.quoteToCreator) : decimal(args.tokenToCreator); const distribution: Entity = { id, diff --git a/subgraphs/launchpad/e2e/run.ts b/subgraphs/launchpad/e2e/run.ts index ca8c607c..7f579ba5 100644 --- a/subgraphs/launchpad/e2e/run.ts +++ b/subgraphs/launchpad/e2e/run.ts @@ -23,6 +23,8 @@ import { import { CHAIN_ID, DEFAULT_SEEDS, + HIGH_QUOTE_TOKEN_ADDRESS, + LOW_QUOTE_TOKEN_ADDRESS, planScenario, stringifyPlan, type LaunchPlan, @@ -53,7 +55,7 @@ interface HardhatArtifact { interface ContractArtifacts { launchpad: HardhatArtifact; launchToken: HardhatArtifact; - weth: HardhatArtifact; + quoteToken: HardhatArtifact; factory: HardhatArtifact; positionManager: HardhatArtifact; pool: HardhatArtifact; @@ -63,8 +65,7 @@ interface Fixture { launchpad: Contract; launchpadAddress: string; launchpadDeploymentBlock: number; - weth: Contract; - wethAddress: string; + quoteTokens: Map; factory: Contract; positionManager: Contract; positionManagerAddress: string; @@ -76,6 +77,7 @@ interface LaunchedToken { tokenKey: string; address: string; pool: string; + quoteToken: string; positionIds: bigint[]; token: Contract; } @@ -359,7 +361,7 @@ async function loadArtifacts( launchToken: await readArtifact( artifact("SushiLaunchpadToken.sol", "SushiLaunchpadToken.json") ), - weth: await readArtifact( + quoteToken: await readArtifact( artifact("test/MockSushiV3.sol", "MockERC20.json") ), factory: await readArtifact( @@ -421,18 +423,26 @@ async function deployFixture( ): Promise { const owner = signers[plan.actors.owner]!; const recipient = signers[plan.actors.protocolRecipients[0]]!; - const wethTemplate = await deploy(artifacts.weth, owner, [ - "Wrapped Ether", - "WETH", + const quoteTokenTemplate = await deploy(artifacts.quoteToken, owner, [ + "Quote Token", + "QUOTE", 18, ]); - const runtimeCode = await provider.getCode(await wethTemplate.getAddress()); - await provider.send("hardhat_setCode", [plan.wethAddress, runtimeCode]); - const weth = new Contract(plan.wethAddress, artifacts.weth.abi, owner); + const runtimeCode = await provider.getCode( + await quoteTokenTemplate.getAddress() + ); + const quoteTokens = new Map(); + for (const address of [LOW_QUOTE_TOKEN_ADDRESS, HIGH_QUOTE_TOKEN_ADDRESS]) { + await provider.send("hardhat_setCode", [address, runtimeCode]); + quoteTokens.set( + address, + new Contract(address, artifacts.quoteToken.abi, owner) + ); + } const factory = await deploy(artifacts.factory, owner); const positionManager = await deploy(artifacts.positionManager, owner, [ await factory.getAddress(), - plan.wethAddress, + LOW_QUOTE_TOKEN_ADDRESS, ]); await (await positionManager.setConsumptionBps(plan.consumptionBps)).wait(); const launchpad = await deploy(artifacts.launchpad, owner, [ @@ -448,8 +458,7 @@ async function deployFixture( launchpad, launchpadAddress: (await launchpad.getAddress()).toLowerCase(), launchpadDeploymentBlock: deploymentReceipt.blockNumber, - weth, - wethAddress: plan.wethAddress.toLowerCase(), + quoteTokens, factory, positionManager, positionManagerAddress: (await positionManager.getAddress()).toLowerCase(), @@ -598,6 +607,7 @@ async function executeScenario( ) as Contract; return await connected.launch( { name: action.name, symbol: action.symbol }, + action.quoteToken, action.ranges, BigInt(block.timestamp + 3_600), { value: action.launchFee } @@ -620,6 +630,16 @@ async function executeScenario( `Missing TokenLaunched for ${action.tokenKey}` ); const launchEvent = launchEvents[0]!; + const quoteToken = String(launchEvent.args.quoteToken).toLowerCase(); + assert.equal( + quoteToken, + action.quoteToken, + `TokenLaunched quote token drifted for ${action.tokenKey}` + ); + assert( + fixture.quoteTokens.has(quoteToken), + `Unknown quote token for ${action.tokenKey}: ${quoteToken}` + ); const positionEvents = events.filter( (event) => event.transactionHash === receipt.hash.toLowerCase() && @@ -635,6 +655,7 @@ async function executeScenario( tokenKey: action.tokenKey, address, pool: String(launchEvent.args.pool).toLowerCase(), + quoteToken, positionIds: positionEvents.map((event) => BigInt(event.args.positionId as bigint) ), @@ -693,17 +714,19 @@ async function executeScenario( const tokenIs0 = String(await pool.token0()).toLowerCase() === token.address.toLowerCase(); + const quoteToken = fixture.quoteTokens.get(token.quoteToken); + assert(quoteToken, `Unknown quote token ${token.quoteToken}`); await ( - await fixture.weth.mint( + await quoteToken.mint( fixture.positionManagerAddress, - action.wethAmount + action.quoteAmount ) ).wait(); await ( await fixture.positionManager.seedFees( token.positionIds[action.positionIndex], - tokenIs0 ? action.tokenAmount : action.wethAmount, - tokenIs0 ? action.wethAmount : action.tokenAmount + tokenIs0 ? action.tokenAmount : action.quoteAmount, + tokenIs0 ? action.quoteAmount : action.tokenAmount ) ).wait(); const connected = fixture.launchpad.connect( @@ -835,7 +858,7 @@ async function waitForIndexedHead( const snapshotQuery = ` query E2ESnapshot { launchpads(first: 1000, orderBy: id) { - id chainId address quoteToken positionManager protocolRecipient launchFee + id chainId address positionManager protocolRecipient launchFee defaultSushiFeeBps protocolReserveBps tokenCount creatorCount positionCount tokens { id } pools { id } creators { id } launchFeeWithdrawals { id } } @@ -844,7 +867,7 @@ const snapshotQuery = ` } tokens(first: 1000, orderBy: id) { id chainId address launchpad { id } creator { id } pool { id } - name symbol decimals totalSupply sushiFeeBps reserveBps reserveAmount + quoteToken name symbol decimals totalSupply sushiFeeBps reserveBps reserveAmount reserveUnlockAt reserveWithdrawn reserveWithdrawal { id } totalAmount0Collected totalAmount1Collected totalAmount0ToSushi totalAmount1ToSushi totalAmount0ToCreator totalAmount1ToCreator @@ -918,7 +941,6 @@ async function runSeed( const expected = buildExpectedSnapshot({ chainId: CHAIN_ID, launchpadAddress: fixture.launchpadAddress, - quoteToken: fixture.wethAddress, positionManager: fixture.positionManagerAddress, protocolRecipient: String(await fixture.launchpad.protocolRecipient()), launchFee: BigInt(await fixture.launchpad.launchFee()), diff --git a/subgraphs/launchpad/e2e/scenario.test.ts b/subgraphs/launchpad/e2e/scenario.test.ts index b6b786dc..629e7fae 100644 --- a/subgraphs/launchpad/e2e/scenario.test.ts +++ b/subgraphs/launchpad/e2e/scenario.test.ts @@ -24,6 +24,12 @@ test("the default matrix satisfies all scenario invariants", () => { for (const seed of DEFAULT_SEEDS) { const plan = planScenario(seed); assert.doesNotThrow(() => validateScenario(plan)); + const launches = plan.actions + .flatMap((action) => + action.kind === "sameBlock" ? action.actions : [action] + ) + .filter((action) => action.kind === "launch"); + assert.equal(new Set(launches.map((launch) => launch.quoteToken)).size, 2); } }); diff --git a/subgraphs/launchpad/e2e/scenario.ts b/subgraphs/launchpad/e2e/scenario.ts index f6a32dc3..532a5a86 100644 --- a/subgraphs/launchpad/e2e/scenario.ts +++ b/subgraphs/launchpad/e2e/scenario.ts @@ -8,8 +8,10 @@ export const INITIAL_SUSHI_FEE_BPS = 7_000; export const INITIAL_RESERVE_BPS = 300; export const RESERVE_LOCK_SECONDS = 365 * 24 * 60 * 60; -export const LOW_WETH_ADDRESS = "0x0000000000000000000000000000000000001000"; -export const HIGH_WETH_ADDRESS = "0xfffffffffffffffffffffffffffffffffffffffe"; +export const LOW_QUOTE_TOKEN_ADDRESS = + "0x0000000000000000000000000000000000001000"; +export const HIGH_QUOTE_TOKEN_ADDRESS = + "0xfffffffffffffffffffffffffffffffffffffffe"; export interface SaleRangePlan { startTick: number; @@ -21,6 +23,7 @@ export interface LaunchPlan { kind: "launch"; tokenKey: string; creatorAccount: number; + quoteToken: string; name: string; symbol: string; ranges: SaleRangePlan[]; @@ -47,7 +50,7 @@ export type ScenarioAction = tokenKey: string; callerAccount: number; positionIndex: number; - wethAmount: bigint; + quoteAmount: bigint; tokenAmount: bigint; } | { kind: "withdrawLaunchFees" } @@ -56,7 +59,6 @@ export type ScenarioAction = export interface ScenarioPlan { seed: number; - wethAddress: string; consumptionBps: number; actors: { owner: 0; @@ -122,6 +124,8 @@ function launchPlan( kind: "launch", tokenKey: `token-${index}`, creatorAccount, + quoteToken: + index % 2 === 0 ? HIGH_QUOTE_TOKEN_ADDRESS : LOW_QUOTE_TOKEN_ADDRESS, name: `Launchpad Agent ${index + 1}`, symbol: `LPA${index + 1}`, ranges, @@ -140,7 +144,7 @@ function distribution( tokenKey, callerAccount, positionIndex: random.int(0, maxPositionIndex), - wethAmount: BigInt(random.int(51, 500)), + quoteAmount: BigInt(random.int(51, 500)), tokenAmount: BigInt(random.int(51, 500)), }; } @@ -263,7 +267,6 @@ export function planScenario(seed: number): ScenarioPlan { const plan: ScenarioPlan = { seed, - wethAddress: seed % 2 === 0 ? HIGH_WETH_ADDRESS : LOW_WETH_ADDRESS, consumptionBps: [10_000, 9_999, 9_750][seed % 3]!, actors: { owner: 0, @@ -294,6 +297,16 @@ export function validateScenario(plan: ScenarioPlan): void { if (new Set(launches.map((launch) => launch.creatorAccount)).size !== 3) { throw new Error("Every scenario must contain exactly three creators"); } + const quoteTokens = new Set(launches.map((launch) => launch.quoteToken)); + if ( + quoteTokens.size !== 2 || + !quoteTokens.has(LOW_QUOTE_TOKEN_ADDRESS) || + !quoteTokens.has(HIGH_QUOTE_TOKEN_ADDRESS) + ) { + throw new Error( + "Every scenario must launch against both quote-token orderings" + ); + } if (!launchKeys.has(plan.intentionallyUnwithdrawnToken)) { throw new Error("The intentionally unwithdrawn token must be launched"); } diff --git a/subgraphs/launchpad/schema.graphql b/subgraphs/launchpad/schema.graphql index f2f7d0de..ac7b68b4 100644 --- a/subgraphs/launchpad/schema.graphql +++ b/subgraphs/launchpad/schema.graphql @@ -5,7 +5,6 @@ type Launchpad @entity { address: Bytes! # Deployment configuration - quoteToken: Bytes! positionManager: Bytes! # Current protocol configuration @@ -50,6 +49,7 @@ type Token @entity { launchpad: Launchpad! creator: Creator! pool: Pool! + quoteToken: Bytes! # ERC-20 metadata name: String! diff --git a/subgraphs/launchpad/src/mappings/helpers.ts b/subgraphs/launchpad/src/mappings/helpers.ts index d765dd79..77a68e0a 100644 --- a/subgraphs/launchpad/src/mappings/helpers.ts +++ b/subgraphs/launchpad/src/mappings/helpers.ts @@ -66,7 +66,6 @@ export function getOrCreateLaunchpad( launchpad = new Launchpad(id); launchpad.chainId = context.chainId; launchpad.address = factory; - launchpad.quoteToken = contract.WETH(); launchpad.positionManager = contract.positionManager(); launchpad.protocolRecipient = contract.protocolRecipient(); launchpad.launchFee = contract.launchFee(); diff --git a/subgraphs/launchpad/src/mappings/pool.ts b/subgraphs/launchpad/src/mappings/pool.ts index 3227e7b3..43ea7cb1 100644 --- a/subgraphs/launchpad/src/mappings/pool.ts +++ b/subgraphs/launchpad/src/mappings/pool.ts @@ -24,12 +24,11 @@ export function handlePositionCreated(event: PositionCreatedEvent): void { const contract = SushiV3Pool.bind(event.params.pool); const token0 = contract.token0(); const token1 = contract.token1(); + const tokenIs0 = token0.equals(event.params.token); + const tokenIs1 = token1.equals(event.params.token); assert( - (token0.equals(event.params.token) && - token1.equals(launchpad.quoteToken)) || - (token0.equals(launchpad.quoteToken) && - token1.equals(event.params.token)), - "Launch position references unexpected pool pair " + idForPool + tokenIs0 != tokenIs1, + "Launch position token must be exactly one side of pool " + idForPool ); pool = new Pool(idForPool); pool.chainId = context.chainId; @@ -46,12 +45,13 @@ export function handlePositionCreated(event: PositionCreatedEvent): void { pool.creationBlockHash = event.block.hash; pool.createdAt = event.block.timestamp; } else { + const tokenIs0 = pool.token0.equals(event.params.token); + const tokenIs1 = pool.token1.equals(event.params.token); assert( pool.launchpad == launchpad.id && pool.address.equals(event.params.pool) && pool.positionManager.equals(launchpad.positionManager) && - (pool.token0.equals(event.params.token) || - pool.token1.equals(event.params.token)), + tokenIs0 != tokenIs1, "Inconsistent ordered position events for pool " + idForPool ); } diff --git a/subgraphs/launchpad/src/mappings/token.ts b/subgraphs/launchpad/src/mappings/token.ts index 4a41c419..23c200d6 100644 --- a/subgraphs/launchpad/src/mappings/token.ts +++ b/subgraphs/launchpad/src/mappings/token.ts @@ -35,8 +35,10 @@ export function handleTokenLaunched(event: TokenLaunchedEvent): void { const pool = requirePool(context, event.params.pool); assert( pool.launchpad == launchpad.id && - (pool.token0.equals(event.params.token) || - pool.token1.equals(event.params.token)) && + ((pool.token0.equals(event.params.token) && + pool.token1.equals(event.params.quoteToken)) || + (pool.token1.equals(event.params.token) && + pool.token0.equals(event.params.quoteToken))) && event.params.positionCount.equals(BigInt.fromI32(pool.positionCount)), "Token launch does not reconcile with pool " + pool.id ); @@ -61,6 +63,7 @@ export function handleTokenLaunched(event: TokenLaunchedEvent): void { token.address = event.params.token; token.creator = creator.id; token.pool = pool.id; + token.quoteToken = event.params.quoteToken; token.name = event.params.name; token.symbol = event.params.symbol; token.decimals = event.params.decimals; @@ -115,8 +118,8 @@ export function handleFeesDistributed(event: FeesDistributedEvent): void { token.pool == pool.id && tokenCreator.address.equals(event.params.creator) && token.sushiFeeBps == event.params.sushiFeeBps && - event.params.wethCollected.equals( - event.params.wethToSushi.plus(event.params.wethToCreator) + event.params.quoteCollected.equals( + event.params.quoteToSushi.plus(event.params.quoteToCreator) ) && event.params.tokenCollected.equals( event.params.tokenToSushi.plus(event.params.tokenToCreator) @@ -137,21 +140,21 @@ export function handleFeesDistributed(event: FeesDistributedEvent): void { distribution.sushiFeeBps = event.params.sushiFeeBps; distribution.amount0Collected = tokenIs0 ? event.params.tokenCollected - : event.params.wethCollected; + : event.params.quoteCollected; distribution.amount1Collected = tokenIs0 - ? event.params.wethCollected + ? event.params.quoteCollected : event.params.tokenCollected; distribution.amount0ToSushi = tokenIs0 ? event.params.tokenToSushi - : event.params.wethToSushi; + : event.params.quoteToSushi; distribution.amount1ToSushi = tokenIs0 - ? event.params.wethToSushi + ? event.params.quoteToSushi : event.params.tokenToSushi; distribution.amount0ToCreator = tokenIs0 ? event.params.tokenToCreator - : event.params.wethToCreator; + : event.params.quoteToCreator; distribution.amount1ToCreator = tokenIs0 - ? event.params.wethToCreator + ? event.params.quoteToCreator : event.params.tokenToCreator; distribution.transactionHash = event.transaction.hash; distribution.logIndex = event.logIndex; diff --git a/subgraphs/launchpad/template.yaml b/subgraphs/launchpad/template.yaml index 0cbe7fec..9c903512 100644 --- a/subgraphs/launchpad/template.yaml +++ b/subgraphs/launchpad/template.yaml @@ -36,7 +36,7 @@ dataSources: - name: SushiV3Pool file: ../v3/abis/pool.json eventHandlers: - - event: TokenLaunched(indexed address,indexed address,indexed address,string,string,uint8,uint256,uint16,uint256,uint64,uint16,uint256) + - event: TokenLaunched(indexed address,indexed address,indexed address,address,string,string,uint8,uint256,uint16,uint256,uint64,uint16,uint256) handler: handleTokenLaunched - event: PositionCreated(indexed address,indexed address,indexed uint256,int24,int24,uint256,uint256,uint128) handler: handlePositionCreated diff --git a/subgraphs/launchpad/tests/Launchpad.test.ts b/subgraphs/launchpad/tests/Launchpad.test.ts index e3fe9a3f..1ab9f8b0 100644 --- a/subgraphs/launchpad/tests/Launchpad.test.ts +++ b/subgraphs/launchpad/tests/Launchpad.test.ts @@ -25,6 +25,7 @@ import { PROTOCOL_RECIPIENT, PROTOCOL_RECIPIENT_TWO, QUOTE_TOKEN, + QUOTE_TOKEN_TWO, TOKEN, TOKEN_THREE, TOKEN_TWO, @@ -81,12 +82,6 @@ test("builds Launchpad, Token, Pool, and canonical initial positions", () => { "address", FACTORY.toHexString() ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "quoteToken", - QUOTE_TOKEN.toHexString() - ); assert.fieldEquals( "Launchpad", LAUNCHPAD_ID, @@ -112,6 +107,12 @@ test("builds Launchpad, Token, Pool, and canonical initial positions", () => { assert.fieldEquals("Token", TOKEN_ID, "launchpad", LAUNCHPAD_ID); assert.fieldEquals("Token", TOKEN_ID, "creator", CREATOR_ID); assert.fieldEquals("Token", TOKEN_ID, "pool", POOL_ID); + assert.fieldEquals( + "Token", + TOKEN_ID, + "quoteToken", + QUOTE_TOKEN.toHexString() + ); assert.fieldEquals("Token", TOKEN_ID, "name", "Sushi Test"); assert.fieldEquals("Token", TOKEN_ID, "symbol", "SUSHIT"); assert.fieldEquals("Token", TOKEN_ID, "decimals", "18"); @@ -152,12 +153,19 @@ test("counts distinct creators and canonicalizes either token ordering", () => { createTokenLaunched(1, 21, TOKEN_TWO, OTHER_POOL, CREATOR) ); - mockV3Pool(POOL_THREE, QUOTE_TOKEN, TOKEN_THREE); + mockV3Pool(POOL_THREE, TOKEN_THREE, QUOTE_TOKEN_TWO); handlePositionCreated( createPositionCreated(103, 30, POOL_THREE, TOKEN_THREE) ); handleTokenLaunched( - createTokenLaunched(1, 31, TOKEN_THREE, POOL_THREE, CREATOR_TWO) + createTokenLaunched( + 1, + 31, + TOKEN_THREE, + POOL_THREE, + CREATOR_TWO, + QUOTE_TOKEN_TWO + ) ); assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "tokenCount", "3"); @@ -166,6 +174,20 @@ test("counts distinct creators and canonicalizes either token ordering", () => { assert.fieldEquals("Creator", CREATOR_ID, "tokenCount", "2"); const secondPoolId = CHAIN_ID.toString() + ":" + OTHER_POOL.toHexString(); + const secondTokenId = CHAIN_ID.toString() + ":" + TOKEN_TWO.toHexString(); + const thirdTokenId = CHAIN_ID.toString() + ":" + TOKEN_THREE.toHexString(); + assert.fieldEquals( + "Token", + secondTokenId, + "quoteToken", + QUOTE_TOKEN.toHexString() + ); + assert.fieldEquals( + "Token", + thirdTokenId, + "quoteToken", + QUOTE_TOKEN_TWO.toHexString() + ); assert.fieldEquals("Pool", secondPoolId, "token0", QUOTE_TOKEN.toHexString()); assert.fieldEquals("Pool", secondPoolId, "token1", TOKEN_TWO.toHexString()); assert.fieldEquals( @@ -233,7 +255,7 @@ test("tracks current Launchpad defaults", () => { test("keeps token launch snapshots scoped to Token", () => { handlePositionCreated(createPositionCreated(101, 10)); handleTokenLaunched( - createTokenLaunched(1, 11, TOKEN, POOL, CREATOR, 6_000, 500) + createTokenLaunched(1, 11, TOKEN, POOL, CREATOR, QUOTE_TOKEN, 6_000, 500) ); assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "7000"); @@ -393,3 +415,14 @@ test( }, true ); + +test( + "fails deterministically when the emitted quote token does not match the pool", + () => { + handlePositionCreated(createPositionCreated(101, 10)); + handleTokenLaunched( + createTokenLaunched(1, 11, TOKEN, POOL, CREATOR, QUOTE_TOKEN_TWO) + ); + }, + true +); diff --git a/subgraphs/launchpad/tests/mocks.ts b/subgraphs/launchpad/tests/mocks.ts index 3d08a3b5..60346d2f 100644 --- a/subgraphs/launchpad/tests/mocks.ts +++ b/subgraphs/launchpad/tests/mocks.ts @@ -62,6 +62,9 @@ export const POOL_THREE = Address.fromString( export const QUOTE_TOKEN = Address.fromString( "0x8000000000000000000000000000000000000008" ); +export const QUOTE_TOKEN_TWO = Address.fromString( + "0xf00000000000000000000000000000000000000f" +); export const POSITION_MANAGER = Address.fromString( "0x9000000000000000000000000000000000000009" ); @@ -71,9 +74,6 @@ export function mockDeploymentContext(): void { context.setBigInt("chainId", CHAIN_ID); dataSourceMock.setAddressAndContext(FACTORY.toHexString(), context); - createMockedFunction(FACTORY, "WETH", "WETH():(address)").returns([ - ethereum.Value.fromAddress(QUOTE_TOKEN), - ]); createMockedFunction( FACTORY, "positionManager", @@ -178,6 +178,7 @@ export function createTokenLaunched( token: Address = TOKEN, pool: Address = POOL, creator: Address = CREATOR, + quoteToken: Address = QUOTE_TOKEN, initialSushiFeeBps: i32 = 7_000, reserveBps: i32 = 300 ): TokenLaunched { @@ -193,6 +194,12 @@ export function createTokenLaunched( event.parameters.push( new ethereum.EventParam("pool", ethereum.Value.fromAddress(pool)) ); + event.parameters.push( + new ethereum.EventParam( + "quoteToken", + ethereum.Value.fromAddress(quoteToken) + ) + ); event.parameters.push( new ethereum.EventParam("name", ethereum.Value.fromString("Sushi Test")) ); @@ -390,7 +397,7 @@ export function createFeesDistributed( ); event.parameters.push( new ethereum.EventParam( - "wethCollected", + "quoteCollected", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(100)) ) ); @@ -402,7 +409,7 @@ export function createFeesDistributed( ); event.parameters.push( new ethereum.EventParam( - "wethToSushi", + "quoteToSushi", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(65)) ) ); @@ -414,7 +421,7 @@ export function createFeesDistributed( ); event.parameters.push( new ethereum.EventParam( - "wethToCreator", + "quoteToCreator", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(35)) ) ); From ad0f60e03655a0276e36efede81091df880b4c68 Mon Sep 17 00:00:00 2001 From: LufyCZ Date: Fri, 24 Jul 2026 13:59:26 +0000 Subject: [PATCH 5/7] chore: configure Robinhood launchpad deployment --- config/robinhood.js | 6 +++--- subgraphs/launchpad/README.md | 28 ++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/config/robinhood.js b/config/robinhood.js index 54f5da33..0d69e167 100644 --- a/config/robinhood.js +++ b/config/robinhood.js @@ -47,11 +47,11 @@ module.exports = { launchpad: { deployments: [ { - // Replace the pending address and block when SushiLaunchpad v1 is deployed. + // SushiLaunchpad v1 production deployment. name: "SushiLaunchpad", chainId: 4663, - address: "0x0000000000000000000000000000000000000000", - startBlock: 0, + address: "0x30DD6230EAD9312D5d00AD58EF6eF6A0093B0554", + startBlock: 18149814, }, ], }, diff --git a/subgraphs/launchpad/README.md b/subgraphs/launchpad/README.md index 5b0e1753..a3e04f2f 100644 --- a/subgraphs/launchpad/README.md +++ b/subgraphs/launchpad/README.md @@ -68,9 +68,24 @@ entity loading, `pool.ts` owns pool discovery and initial launch positions, `token.ts` owns token-level flows, and `launchpad.ts` owns factory-wide settings and provides the manifest exports. -Robinhood currently contains a zero-address build placeholder because the -production factory address and deployment block are still an explicit launch -gate in the contract specification. Replace both values before deployment. +Robinhood is configured for the SushiLaunchpad v1 production deployment at +`0x30DD6230EAD9312D5d00AD58EF6eF6A0093B0554`, starting at block `18149814`. + +## Goldsky deployment + +Generate and validate the Robinhood manifest, then deploy it to the existing +`sushiswap` Goldsky project: + +```sh +NETWORK=robinhood pnpm --filter launchpad generate +pnpm --filter launchpad build +goldsky subgraph deploy sushiswap/launchpad-robinhood/1.0.0 \ + --path subgraphs/launchpad/build \ + --description "Sushi Launchpad on Robinhood" +``` + +The generated `subgraph.yaml` and `build/` output are ignored build artifacts; +regenerate them after checking out a fresh tree. ## Development @@ -80,9 +95,10 @@ pnpm build pnpm test ``` -`abis/SushiLaunchpad.json` is pinned from the `SushiLaunchpad` artifact at -contract commit `ee5f8bb`. Refresh it together with mappings and tests when an -indexed event changes. +`abis/SushiLaunchpad.json` is ABI-equivalent to the current `SushiLaunchpad` +artifact at contract commit `050bfbd`; the indexed interface is unchanged from +the previous `ee5f8bb` revision. Refresh it together with mappings and tests +when an indexed event changes. ## Local end-to-end test From 85b5678621283c7542cb572b36fb9e3b33d470c6 Mon Sep 17 00:00:00 2001 From: LufyCZ Date: Sat, 25 Jul 2026 01:44:09 +0000 Subject: [PATCH 6/7] feat(launchpad): expose canonical hex strings --- subgraphs/launchpad/README.md | 11 ++-- subgraphs/launchpad/e2e/model.ts | 17 ++++++ subgraphs/launchpad/e2e/run.ts | 28 +++++---- subgraphs/launchpad/schema.graphql | 17 ++++++ subgraphs/launchpad/src/mappings/helpers.ts | 13 +++- subgraphs/launchpad/src/mappings/launchpad.ts | 8 ++- subgraphs/launchpad/src/mappings/pool.ts | 10 ++++ subgraphs/launchpad/src/mappings/token.ts | 6 ++ subgraphs/launchpad/tests/Launchpad.test.ts | 60 +++++++++++++++++++ 9 files changed, 151 insertions(+), 19 deletions(-) diff --git a/subgraphs/launchpad/README.md b/subgraphs/launchpad/README.md index a3e04f2f..f2dd3faa 100644 --- a/subgraphs/launchpad/README.md +++ b/subgraphs/launchpad/README.md @@ -30,8 +30,11 @@ needed: the pool and positions are permanent domain records even though the contract emits `TokenLaunched` last. Every stable entity ID is chain-scoped even though each deployment indexes one -network. Addresses are stored as `Bytes`, whose GraphQL representation is -normalized lowercase hexadecimal. +network. Address and hash values remain available as `Bytes` for Graph-native +consumers. The five entities mirrored into data-api also expose parallel +`*Hex` string fields containing canonical lowercase, `0x`-prefixed hexadecimal. +Those strings avoid connector-specific binary encodings such as PostgreSQL +bytea's `\x` prefix and are the source of truth for text columns downstream. ## Configuration @@ -79,9 +82,9 @@ Generate and validate the Robinhood manifest, then deploy it to the existing ```sh NETWORK=robinhood pnpm --filter launchpad generate pnpm --filter launchpad build -goldsky subgraph deploy sushiswap/launchpad-robinhood/1.0.0 \ +goldsky subgraph deploy sushiswap/launchpad-robinhood-v2 \ --path subgraphs/launchpad/build \ - --description "Sushi Launchpad on Robinhood" + --description "Sushi Launchpad on Robinhood with canonical hex strings" ``` The generated `subgraph.yaml` and `build/` output are ignored build artifacts; diff --git a/subgraphs/launchpad/e2e/model.ts b/subgraphs/launchpad/e2e/model.ts index 8582e14e..5d81ac8a 100644 --- a/subgraphs/launchpad/e2e/model.ts +++ b/subgraphs/launchpad/e2e/model.ts @@ -100,8 +100,11 @@ export function buildExpectedSnapshot( id: launchpadId, chainId: input.chainId.toString(), address: input.launchpadAddress.toLowerCase(), + addressHex: input.launchpadAddress.toLowerCase(), positionManager: input.positionManager.toLowerCase(), + positionManagerHex: input.positionManager.toLowerCase(), protocolRecipient: input.protocolRecipient.toLowerCase(), + protocolRecipientHex: input.protocolRecipient.toLowerCase(), launchFee: input.launchFee.toString(), defaultSushiFeeBps: input.defaultSushiFeeBps, protocolReserveBps: input.protocolReserveBps, @@ -145,16 +148,22 @@ export function buildExpectedSnapshot( id: idForPool, chainId: input.chainId.toString(), address: poolAddress, + addressHex: poolAddress, launchpad: relation(launchpadId), token0: poolConfiguration.token0.toLowerCase(), + token0Hex: poolConfiguration.token0.toLowerCase(), token1: poolConfiguration.token1.toLowerCase(), + token1Hex: poolConfiguration.token1.toLowerCase(), fee: poolConfiguration.fee, tickSpacing: poolConfiguration.tickSpacing, positionManager: input.positionManager.toLowerCase(), + positionManagerHex: input.positionManager.toLowerCase(), positionCount: 0, creationTransactionHash: event.transactionHash.toLowerCase(), + creationTransactionHashHex: event.transactionHash.toLowerCase(), creationBlockNumber: event.blockNumber.toString(), creationBlockHash: event.blockHash.toLowerCase(), + creationBlockHashHex: event.blockHash.toLowerCase(), createdAt: event.timestamp.toString(), positions: [], }; @@ -176,6 +185,7 @@ export function buildExpectedSnapshot( id: idForPosition, chainId: input.chainId.toString(), positionManager: input.positionManager.toLowerCase(), + positionManagerHex: input.positionManager.toLowerCase(), positionId: decimal(args.positionId), index, pool: relation(idForPool), @@ -187,9 +197,11 @@ export function buildExpectedSnapshot( amount0: tokenIs0 ? used : zero, amount1: tokenIs0 ? zero : used, creationTransactionHash: event.transactionHash.toLowerCase(), + creationTransactionHashHex: event.transactionHash.toLowerCase(), creationLogIndex: event.logIndex.toString(), creationBlockNumber: event.blockNumber.toString(), creationBlockHash: event.blockHash.toLowerCase(), + creationBlockHashHex: event.blockHash.toLowerCase(), createdAt: event.timestamp.toString(), }; launchPositions.set(idForPosition, position); @@ -225,6 +237,7 @@ export function buildExpectedSnapshot( id: idForCreator, chainId: input.chainId.toString(), address: lower(args.creator), + addressHex: lower(args.creator), launchpad: relation(launchpadId), tokenCount: 0, tokens: [], @@ -239,10 +252,12 @@ export function buildExpectedSnapshot( id: idForToken, chainId: input.chainId.toString(), address: lower(args.token), + addressHex: lower(args.token), launchpad: relation(launchpadId), creator: relation(idForCreator), pool: relation(idForPool), quoteToken, + quoteTokenHex: quoteToken, name: String(args.name), symbol: String(args.symbol), decimals: integer(args.decimals), @@ -261,9 +276,11 @@ export function buildExpectedSnapshot( totalAmount1ToCreator: "0", feeDistributions: [], creationTransactionHash: event.transactionHash.toLowerCase(), + creationTransactionHashHex: event.transactionHash.toLowerCase(), creationLogIndex: event.logIndex.toString(), creationBlockNumber: event.blockNumber.toString(), creationBlockHash: event.blockHash.toLowerCase(), + creationBlockHashHex: event.blockHash.toLowerCase(), createdAt: event.timestamp.toString(), }; tokens.set(idForToken, token); diff --git a/subgraphs/launchpad/e2e/run.ts b/subgraphs/launchpad/e2e/run.ts index 7f579ba5..9f19ba47 100644 --- a/subgraphs/launchpad/e2e/run.ts +++ b/subgraphs/launchpad/e2e/run.ts @@ -858,33 +858,37 @@ async function waitForIndexedHead( const snapshotQuery = ` query E2ESnapshot { launchpads(first: 1000, orderBy: id) { - id chainId address positionManager protocolRecipient launchFee + id chainId address addressHex positionManager positionManagerHex + protocolRecipient protocolRecipientHex launchFee defaultSushiFeeBps protocolReserveBps tokenCount creatorCount positionCount tokens { id } pools { id } creators { id } launchFeeWithdrawals { id } } creators(first: 1000, orderBy: id) { - id chainId address launchpad { id } tokenCount tokens { id } + id chainId address addressHex launchpad { id } tokenCount tokens { id } } tokens(first: 1000, orderBy: id) { - id chainId address launchpad { id } creator { id } pool { id } - quoteToken name symbol decimals totalSupply sushiFeeBps reserveBps reserveAmount + id chainId address addressHex launchpad { id } creator { id } pool { id } + quoteToken quoteTokenHex name symbol decimals totalSupply sushiFeeBps + reserveBps reserveAmount reserveUnlockAt reserveWithdrawn reserveWithdrawal { id } totalAmount0Collected totalAmount1Collected totalAmount0ToSushi totalAmount1ToSushi totalAmount0ToCreator totalAmount1ToCreator feeDistributions { id } - creationTransactionHash creationLogIndex creationBlockNumber - creationBlockHash createdAt + creationTransactionHash creationTransactionHashHex creationLogIndex + creationBlockNumber creationBlockHash creationBlockHashHex createdAt } pools(first: 1000, orderBy: id) { - id chainId address launchpad { id } positions { id } - token0 token1 fee tickSpacing positionManager positionCount - creationTransactionHash creationBlockNumber creationBlockHash createdAt + id chainId address addressHex launchpad { id } positions { id } + token0 token0Hex token1 token1Hex fee tickSpacing + positionManager positionManagerHex positionCount + creationTransactionHash creationTransactionHashHex creationBlockNumber + creationBlockHash creationBlockHashHex createdAt } launchPositions(first: 1000, orderBy: id) { - id chainId positionManager positionId index pool { id } + id chainId positionManager positionManagerHex positionId index pool { id } tickLower tickUpper liquidity amount0Desired amount1Desired amount0 amount1 - creationTransactionHash creationLogIndex creationBlockNumber - creationBlockHash createdAt + creationTransactionHash creationTransactionHashHex creationLogIndex + creationBlockNumber creationBlockHash creationBlockHashHex createdAt } feeDistributions(first: 1000, orderBy: id) { id chainId token { id } pool { id } caller sushiRecipient creatorRecipient diff --git a/subgraphs/launchpad/schema.graphql b/subgraphs/launchpad/schema.graphql index ac7b68b4..c1bcfeef 100644 --- a/subgraphs/launchpad/schema.graphql +++ b/subgraphs/launchpad/schema.graphql @@ -3,12 +3,15 @@ type Launchpad @entity { id: ID! chainId: BigInt! address: Bytes! + addressHex: String! # Deployment configuration positionManager: Bytes! + positionManagerHex: String! # Current protocol configuration protocolRecipient: Bytes! + protocolRecipientHex: String! launchFee: BigInt! defaultSushiFeeBps: Int! protocolReserveBps: Int! @@ -30,6 +33,7 @@ type Creator @entity { id: ID! chainId: BigInt! address: Bytes! + addressHex: String! # Relationships launchpad: Launchpad! @@ -44,12 +48,14 @@ type Token @entity { id: ID! chainId: BigInt! address: Bytes! + addressHex: String! # Relationships launchpad: Launchpad! creator: Creator! pool: Pool! quoteToken: Bytes! + quoteTokenHex: String! # ERC-20 metadata name: String! @@ -78,9 +84,11 @@ type Token @entity { # Creation metadata creationTransactionHash: Bytes! + creationTransactionHashHex: String! creationLogIndex: BigInt! creationBlockNumber: BigInt! creationBlockHash: Bytes! + creationBlockHashHex: String! createdAt: BigInt! } @@ -89,6 +97,7 @@ type Pool @entity { id: ID! chainId: BigInt! address: Bytes! + addressHex: String! # Relationships launchpad: Launchpad! @@ -96,18 +105,23 @@ type Pool @entity { # Immutable pool configuration token0: Bytes! + token0Hex: String! token1: Bytes! + token1Hex: String! fee: Int! tickSpacing: Int! positionManager: Bytes! + positionManagerHex: String! # Aggregate metrics positionCount: Int! # Creation metadata creationTransactionHash: Bytes! + creationTransactionHashHex: String! creationBlockNumber: BigInt! creationBlockHash: Bytes! + creationBlockHashHex: String! createdAt: BigInt! } @@ -116,6 +130,7 @@ type LaunchPosition @entity(immutable: true) { id: ID! chainId: BigInt! positionManager: Bytes! + positionManagerHex: String! positionId: BigInt! index: Int! @@ -135,9 +150,11 @@ type LaunchPosition @entity(immutable: true) { # Creation metadata creationTransactionHash: Bytes! + creationTransactionHashHex: String! creationLogIndex: BigInt! creationBlockNumber: BigInt! creationBlockHash: Bytes! + creationBlockHashHex: String! createdAt: BigInt! } diff --git a/subgraphs/launchpad/src/mappings/helpers.ts b/subgraphs/launchpad/src/mappings/helpers.ts index 77a68e0a..406a687c 100644 --- a/subgraphs/launchpad/src/mappings/helpers.ts +++ b/subgraphs/launchpad/src/mappings/helpers.ts @@ -30,6 +30,10 @@ export function addressId(chainId: BigInt, address: Bytes): string { return chainId.toString().concat(":").concat(address.toHexString()); } +export function canonicalHex(value: Bytes): string { + return value.toHexString().toLowerCase(); +} + export function creatorId(launchpad: Launchpad, address: Bytes): string { return launchpad.id.concat(":").concat(address.toHexString()); } @@ -65,9 +69,14 @@ export function getOrCreateLaunchpad( const contract = SushiLaunchpadContract.bind(factory); launchpad = new Launchpad(id); launchpad.chainId = context.chainId; + const positionManager = contract.positionManager(); + const protocolRecipient = contract.protocolRecipient(); launchpad.address = factory; - launchpad.positionManager = contract.positionManager(); - launchpad.protocolRecipient = contract.protocolRecipient(); + launchpad.addressHex = canonicalHex(factory); + launchpad.positionManager = positionManager; + launchpad.positionManagerHex = canonicalHex(positionManager); + launchpad.protocolRecipient = protocolRecipient; + launchpad.protocolRecipientHex = canonicalHex(protocolRecipient); launchpad.launchFee = contract.launchFee(); launchpad.defaultSushiFeeBps = contract.defaultSushiFeeBps(); launchpad.protocolReserveBps = contract.protocolReserveBps(); diff --git a/subgraphs/launchpad/src/mappings/launchpad.ts b/subgraphs/launchpad/src/mappings/launchpad.ts index acdbabcd..b9e0f8ac 100644 --- a/subgraphs/launchpad/src/mappings/launchpad.ts +++ b/subgraphs/launchpad/src/mappings/launchpad.ts @@ -6,7 +6,12 @@ import { ProtocolReserveBpsUpdated as ProtocolReserveBpsUpdatedEvent, } from "../../generated/SushiLaunchpad/SushiLaunchpad"; import { LaunchFeeWithdrawal } from "../../generated/schema"; -import { deploymentContext, eventId, getOrCreateLaunchpad } from "./helpers"; +import { + canonicalHex, + deploymentContext, + eventId, + getOrCreateLaunchpad, +} from "./helpers"; const BPS_DENOMINATOR = 10_000; const MAX_PROTOCOL_RESERVE_BPS = 1_000; @@ -45,6 +50,7 @@ export function handleProtocolRecipientUpdated( const context = deploymentContext(); const launchpad = getOrCreateLaunchpad(context, event.address); launchpad.protocolRecipient = event.params.newRecipient; + launchpad.protocolRecipientHex = canonicalHex(event.params.newRecipient); launchpad.save(); } diff --git a/subgraphs/launchpad/src/mappings/pool.ts b/subgraphs/launchpad/src/mappings/pool.ts index 43ea7cb1..a979f85f 100644 --- a/subgraphs/launchpad/src/mappings/pool.ts +++ b/subgraphs/launchpad/src/mappings/pool.ts @@ -4,6 +4,7 @@ import { SushiV3Pool } from "../../generated/SushiLaunchpad/SushiV3Pool"; import { LaunchPosition, Pool, Token } from "../../generated/schema"; import { addressId, + canonicalHex, deploymentContext, getOrCreateLaunchpad, positionId, @@ -34,15 +35,21 @@ export function handlePositionCreated(event: PositionCreatedEvent): void { pool.chainId = context.chainId; pool.launchpad = launchpad.id; pool.address = event.params.pool; + pool.addressHex = canonicalHex(event.params.pool); pool.token0 = token0; + pool.token0Hex = canonicalHex(token0); pool.token1 = token1; + pool.token1Hex = canonicalHex(token1); pool.fee = contract.fee(); pool.tickSpacing = contract.tickSpacing(); pool.positionManager = launchpad.positionManager; + pool.positionManagerHex = canonicalHex(launchpad.positionManager); pool.positionCount = 0; pool.creationTransactionHash = event.transaction.hash; + pool.creationTransactionHashHex = canonicalHex(event.transaction.hash); pool.creationBlockNumber = event.block.number; pool.creationBlockHash = event.block.hash; + pool.creationBlockHashHex = canonicalHex(event.block.hash); pool.createdAt = event.block.timestamp; } else { const tokenIs0 = pool.token0.equals(event.params.token); @@ -72,6 +79,7 @@ export function handlePositionCreated(event: PositionCreatedEvent): void { position.chainId = context.chainId; position.pool = pool.id; position.positionManager = launchpad.positionManager; + position.positionManagerHex = canonicalHex(launchpad.positionManager); position.positionId = event.params.positionId; position.index = pool.positionCount; position.tickLower = event.params.tickLower; @@ -82,9 +90,11 @@ export function handlePositionCreated(event: PositionCreatedEvent): void { position.amount1 = tokenIs0 ? zero : event.params.tokenUsed; position.liquidity = event.params.liquidity; position.creationTransactionHash = event.transaction.hash; + position.creationTransactionHashHex = canonicalHex(event.transaction.hash); position.creationLogIndex = event.logIndex; position.creationBlockNumber = event.block.number; position.creationBlockHash = event.block.hash; + position.creationBlockHashHex = canonicalHex(event.block.hash); position.createdAt = event.block.timestamp; position.save(); diff --git a/subgraphs/launchpad/src/mappings/token.ts b/subgraphs/launchpad/src/mappings/token.ts index 23c200d6..b0df50cf 100644 --- a/subgraphs/launchpad/src/mappings/token.ts +++ b/subgraphs/launchpad/src/mappings/token.ts @@ -13,6 +13,7 @@ import { } from "../../generated/schema"; import { addressId, + canonicalHex, creatorId, deploymentContext, eventId, @@ -50,6 +51,7 @@ export function handleTokenLaunched(event: TokenLaunchedEvent): void { creator.chainId = context.chainId; creator.launchpad = launchpad.id; creator.address = event.params.creator; + creator.addressHex = canonicalHex(event.params.creator); creator.tokenCount = 0; launchpad.creatorCount += 1; } @@ -61,9 +63,11 @@ export function handleTokenLaunched(event: TokenLaunchedEvent): void { token.chainId = context.chainId; token.launchpad = launchpad.id; token.address = event.params.token; + token.addressHex = canonicalHex(event.params.token); token.creator = creator.id; token.pool = pool.id; token.quoteToken = event.params.quoteToken; + token.quoteTokenHex = canonicalHex(event.params.quoteToken); token.name = event.params.name; token.symbol = event.params.symbol; token.decimals = event.params.decimals; @@ -80,9 +84,11 @@ export function handleTokenLaunched(event: TokenLaunchedEvent): void { token.totalAmount0ToCreator = zero; token.totalAmount1ToCreator = zero; token.creationTransactionHash = event.transaction.hash; + token.creationTransactionHashHex = canonicalHex(event.transaction.hash); token.creationLogIndex = event.logIndex; token.creationBlockNumber = event.block.number; token.creationBlockHash = event.block.hash; + token.creationBlockHashHex = canonicalHex(event.block.hash); token.createdAt = event.block.timestamp; token.save(); diff --git a/subgraphs/launchpad/tests/Launchpad.test.ts b/subgraphs/launchpad/tests/Launchpad.test.ts index 1ab9f8b0..5320e17e 100644 --- a/subgraphs/launchpad/tests/Launchpad.test.ts +++ b/subgraphs/launchpad/tests/Launchpad.test.ts @@ -82,18 +82,36 @@ test("builds Launchpad, Token, Pool, and canonical initial positions", () => { "address", FACTORY.toHexString() ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "addressHex", + FACTORY.toHexString().toLowerCase() + ); assert.fieldEquals( "Launchpad", LAUNCHPAD_ID, "positionManager", POSITION_MANAGER.toHexString() ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "positionManagerHex", + POSITION_MANAGER.toHexString().toLowerCase() + ); assert.fieldEquals( "Launchpad", LAUNCHPAD_ID, "protocolRecipient", PROTOCOL_RECIPIENT.toHexString() ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "protocolRecipientHex", + PROTOCOL_RECIPIENT.toHexString().toLowerCase() + ); assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "launchFee", "500000000000000"); assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "7000"); assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "300"); @@ -101,9 +119,21 @@ test("builds Launchpad, Token, Pool, and canonical initial positions", () => { assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "creatorCount", "1"); assert.fieldEquals("Creator", CREATOR_ID, "address", CREATOR.toHexString()); + assert.fieldEquals( + "Creator", + CREATOR_ID, + "addressHex", + CREATOR.toHexString().toLowerCase() + ); assert.fieldEquals("Creator", CREATOR_ID, "tokenCount", "1"); assert.fieldEquals("Token", TOKEN_ID, "address", TOKEN.toHexString()); + assert.fieldEquals( + "Token", + TOKEN_ID, + "addressHex", + TOKEN.toHexString().toLowerCase() + ); assert.fieldEquals("Token", TOKEN_ID, "launchpad", LAUNCHPAD_ID); assert.fieldEquals("Token", TOKEN_ID, "creator", CREATOR_ID); assert.fieldEquals("Token", TOKEN_ID, "pool", POOL_ID); @@ -113,6 +143,12 @@ test("builds Launchpad, Token, Pool, and canonical initial positions", () => { "quoteToken", QUOTE_TOKEN.toHexString() ); + assert.fieldEquals( + "Token", + TOKEN_ID, + "quoteTokenHex", + QUOTE_TOKEN.toHexString().toLowerCase() + ); assert.fieldEquals("Token", TOKEN_ID, "name", "Sushi Test"); assert.fieldEquals("Token", TOKEN_ID, "symbol", "SUSHIT"); assert.fieldEquals("Token", TOKEN_ID, "decimals", "18"); @@ -123,12 +159,30 @@ test("builds Launchpad, Token, Pool, and canonical initial positions", () => { assert.fieldEquals("Token", TOKEN_ID, "reserveWithdrawn", "false"); assert.fieldEquals("Pool", POOL_ID, "token0", TOKEN.toHexString()); + assert.fieldEquals( + "Pool", + POOL_ID, + "token0Hex", + TOKEN.toHexString().toLowerCase() + ); assert.fieldEquals("Pool", POOL_ID, "token1", QUOTE_TOKEN.toHexString()); + assert.fieldEquals( + "Pool", + POOL_ID, + "token1Hex", + QUOTE_TOKEN.toHexString().toLowerCase() + ); assert.fieldEquals("Pool", POOL_ID, "fee", "10000"); assert.fieldEquals("Pool", POOL_ID, "tickSpacing", "200"); const firstPositionId = POSITION_PREFIX + "101"; assert.fieldEquals("LaunchPosition", firstPositionId, "pool", POOL_ID); + assert.fieldEquals( + "LaunchPosition", + firstPositionId, + "positionManagerHex", + POSITION_MANAGER.toHexString().toLowerCase() + ); assert.fieldEquals("LaunchPosition", firstPositionId, "index", "0"); assert.fieldEquals("LaunchPosition", firstPositionId, "tickLower", "-400"); assert.fieldEquals("LaunchPosition", firstPositionId, "tickUpper", "-200"); @@ -290,6 +344,12 @@ test("tracks Launchpad recipient, launch fee, and fee withdrawals", () => { "protocolRecipient", PROTOCOL_RECIPIENT_TWO.toHexString() ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "protocolRecipientHex", + PROTOCOL_RECIPIENT_TWO.toHexString().toLowerCase() + ); assert.fieldEquals( "Launchpad", LAUNCHPAD_ID, From 3f4c2d654af8727bd90c7102725d29e7765a94e6 Mon Sep 17 00:00:00 2001 From: LufyCZ Date: Tue, 28 Jul 2026 23:59:58 +0000 Subject: [PATCH 7/7] feat: initial launchpad new subgraph --- config/robinhood.js | 4 +- subgraphs/launchpad/README.md | 114 +-- subgraphs/launchpad/abis/SushiLaunchpad.json | 747 ++++++++++++++---- subgraphs/launchpad/e2e/model.ts | 499 ++++++------ subgraphs/launchpad/e2e/run.ts | 252 ++++-- subgraphs/launchpad/e2e/scenario.ts | 70 +- subgraphs/launchpad/schema.graphql | 270 +++---- subgraphs/launchpad/src/mapping.ts | 21 + .../launchpad/src/mappings/launch-activity.ts | 142 ++++ .../src/mappings/launch-invariants.ts | 93 +++ .../src/mappings/launch-lifecycle.ts | 141 ++++ .../launchpad/src/mappings/launch-state.ts | 54 ++ subgraphs/launchpad/src/mappings/launchpad.ts | 75 +- subgraphs/launchpad/src/mappings/pool.ts | 105 --- .../src/mappings/{helpers.ts => shared.ts} | 66 +- subgraphs/launchpad/src/mappings/token.ts | 226 ------ subgraphs/launchpad/template.yaml | 23 +- subgraphs/launchpad/tests/.latest.json | 2 +- subgraphs/launchpad/tests/Launchpad.test.ts | 488 ------------ .../launchpad/tests/launch-activity.test.ts | 84 ++ .../launchpad/tests/launch-lifecycle.test.ts | 276 +++++++ .../launchpad/tests/launch-state.test.ts | 64 ++ subgraphs/launchpad/tests/launchpad.test.ts | 98 +++ subgraphs/launchpad/tests/mocks.ts | 208 ++++- 24 files changed, 2456 insertions(+), 1666 deletions(-) create mode 100644 subgraphs/launchpad/src/mapping.ts create mode 100644 subgraphs/launchpad/src/mappings/launch-activity.ts create mode 100644 subgraphs/launchpad/src/mappings/launch-invariants.ts create mode 100644 subgraphs/launchpad/src/mappings/launch-lifecycle.ts create mode 100644 subgraphs/launchpad/src/mappings/launch-state.ts delete mode 100644 subgraphs/launchpad/src/mappings/pool.ts rename subgraphs/launchpad/src/mappings/{helpers.ts => shared.ts} (51%) delete mode 100644 subgraphs/launchpad/src/mappings/token.ts delete mode 100644 subgraphs/launchpad/tests/Launchpad.test.ts create mode 100644 subgraphs/launchpad/tests/launch-activity.test.ts create mode 100644 subgraphs/launchpad/tests/launch-lifecycle.test.ts create mode 100644 subgraphs/launchpad/tests/launch-state.test.ts create mode 100644 subgraphs/launchpad/tests/launchpad.test.ts diff --git a/config/robinhood.js b/config/robinhood.js index 0d69e167..cc41168d 100644 --- a/config/robinhood.js +++ b/config/robinhood.js @@ -50,8 +50,8 @@ module.exports = { // SushiLaunchpad v1 production deployment. name: "SushiLaunchpad", chainId: 4663, - address: "0x30DD6230EAD9312D5d00AD58EF6eF6A0093B0554", - startBlock: 18149814, + address: "0x104F1Ab42674565EC3DF0BFEbCcC4186f72fA7ED", + startBlock: 21957383, }, ], }, diff --git a/subgraphs/launchpad/README.md b/subgraphs/launchpad/README.md index f2dd3faa..58c3cdd2 100644 --- a/subgraphs/launchpad/README.md +++ b/subgraphs/launchpad/README.md @@ -7,34 +7,44 @@ data-api market projection. ## Entities - `Launchpad` stores one factory deployment, its current protocol recipient, - native launch fee, default Sushi fee, protocol reserve settings, and - aggregate token, creator, and position counts. -- `Creator` provides the per-Launchpad identity needed to count distinct - creators and links each creator to their tokens. -- `Token` stores ERC-20 metadata, its creator, per-launch quote token, current - Sushi fee, reserve state, cumulative fee amounts, and its pool. -- `Pool` stores the immutable V3 configuration (`token0`, `token1`, fee, tick - spacing, and position manager) and links to its initial launch positions. -- `LaunchPosition` stores only the V3 NFTs minted in the token launch - transaction. Desired and used amounts are normalized to `amount0` and - `amount1` according to the pool's canonical token ordering. -- `FeeDistribution` stores each collection and split with both recipients and - all token0/token1 amounts. `ReserveWithdrawal` records the token's one - protocol-reserve withdrawal. -- `LaunchFeeWithdrawal` stores each withdrawal of accumulated native launch - fees with its amount and recipient. - -`Pool.positionCount` also reconciles the ordered `PositionCreated` events with -the final `TokenLaunched.positionCount`. No temporary accumulator entity is -needed: the pool and positions are permanent domain records even though the -contract emits `TokenLaunched` last. + native launch fee, initial USD FDV, default Sushi fee, protocol reserve + settings, position manager, deployment-scoped launch token/pool metadata, and + current per-quote-token price feeds. Setting a feed to the zero address + removes that quote token from the current allowlist projection. +- `Launch` is the canonical current projection for one launched token. It + embeds the current and initial creator, token metadata/economics, pricing + inputs and the factory's initial FDV, aligned start tick, canonical launch + pool, and its one V3 position. +- `PendingLaunch` is an internal two-log accumulator. `TokenLaunched` creates + it; the same transaction's following `PositionCreated` validates it, creates + the complete `Launch`, and removes it. Canonical `Launch` consumers therefore + never observe a falsely complete or positionless record. +- `CreatorTransfer`, `InitialBuy`, `FeeDistribution`, `ReserveWithdrawal`, and + `LaunchFeeWithdrawal` preserve immutable lifecycle/history events and point + directly to their `Launch` or `Launchpad`. + +The `pool` embedded in `Launch` is specifically the immutable pool created for +that launch. A token can have other V3 pools, and downstream market projections +must continue to key pool-specific metrics and trades by pool address rather +than treating the token as one market. + +Exactly four getters are invoked on the first launch for each Launchpad +deployment: `decimals()` and `totalSupply()` on the launched ERC-20, plus +`fee()` and `tickSpacing()` on the emitted canonical V3 pool. The observed +values are cached on `Launchpad`; every later launch from that deployment +reuses the cache with zero token or pool getter calls. `PositionCreated` copies +the cached values into the completed `Launch`, so `PendingLaunch` only carries +the event-sourced data needed to join the two launch logs. +Initial FDV remains factory-sourced: it is read from the Launchpad contract +when the factory projection is initialized and copied to every `Launch`. +Canonical token ordering is derived from the two emitted addresses without +calling `token0()` or `token1()`. Every stable entity ID is chain-scoped even though each deployment indexes one -network. Address and hash values remain available as `Bytes` for Graph-native -consumers. The five entities mirrored into data-api also expose parallel -`*Hex` string fields containing canonical lowercase, `0x`-prefixed hexadecimal. -Those strings avoid connector-specific binary encodings such as PostgreSQL -bytea's `\x` prefix and are the source of truth for text columns downstream. +network. Every stored Ethereum address and hash uses its unsuffixed field name +and is a canonical lowercase, `0x`-prefixed `String`. This avoids +connector-specific binary encodings such as PostgreSQL bytea's `\x` prefix and +gives downstream text columns one unambiguous representation. ## Configuration @@ -55,24 +65,27 @@ launchpad: { ``` All deployments on a network share the schema and mappings. A quote token is -selected independently for each launch and persisted on its `Token` entity. +selected independently for each launch and persisted on its `Launch` entity. The subgraph indexes every valid quote token emitted by the contract; public allowlisting and market filtering belong downstream in data-api. The mapping -reads the position manager, protocol recipient, launch fee, and initial -defaults from the Launchpad contract, then reads canonical token ordering, fee, -and tick spacing from each created V3 pool. Add old and new factory versions to -the array during a rotation; do not replace historical entries. Keep the first -historical data source named `SushiLaunchpad`; the shared mapping imports its -generated ABI bindings, while later entries use unique names such as -`SushiLaunchpadV2`. - -The mapping is split by responsibility: `helpers.ts` owns chain-scoped IDs and -entity loading, `pool.ts` owns pool discovery and initial launch positions, -`token.ts` owns token-level flows, and `launchpad.ts` owns factory-wide settings -and provides the manifest exports. +reads the position manager, protocol recipient, launch fee, initial FDV, and +initial defaults from the Launchpad contract. It derives canonical token +ordering from emitted addresses and observes immutable token and pool values +only for the first launch of each factory deployment. Add old and new factory +versions to the array during a rotation; each deployment has its own metadata +cache, so its first launch makes four getter calls and subsequent launches make +none. Do not replace historical entries. Keep the first historical data source +named `SushiLaunchpad`; the shared mapping imports its generated ABI bindings, +while later entries use unique names such as `SushiLaunchpadV2`. + +The mapping is split by responsibility: `mapping.ts` is the thin manifest +entrypoint; `launch-lifecycle.ts`, `launch-state.ts`, and `launch-activity.ts` +own the launch aggregate; `launch-invariants.ts` owns the typed deployment +cache and its one-time contract reads; `launchpad.ts` owns factory +configuration; and `shared.ts` owns chain-scoped IDs and entity loading. Robinhood is configured for the SushiLaunchpad v1 production deployment at -`0x30DD6230EAD9312D5d00AD58EF6eF6A0093B0554`, starting at block `18149814`. +`0x104F1Ab42674565EC3DF0BFEbCcC4186f72fA7ED`, starting at block `21957383`. ## Goldsky deployment @@ -82,9 +95,9 @@ Generate and validate the Robinhood manifest, then deploy it to the existing ```sh NETWORK=robinhood pnpm --filter launchpad generate pnpm --filter launchpad build -goldsky subgraph deploy sushiswap/launchpad-robinhood-v2 \ +goldsky subgraph deploy sushiswap/launchpad-robinhood \ --path subgraphs/launchpad/build \ - --description "Sushi Launchpad on Robinhood with canonical hex strings" + --description "Sushi Launchpad on Robinhood with canonical address strings" ``` The generated `subgraph.yaml` and `build/` output are ignored build artifacts; @@ -98,10 +111,10 @@ pnpm build pnpm test ``` -`abis/SushiLaunchpad.json` is ABI-equivalent to the current `SushiLaunchpad` -artifact at contract commit `050bfbd`; the indexed interface is unchanged from -the previous `ee5f8bb` revision. Refresh it together with mappings and tests -when an indexed event changes. +`abis/SushiLaunchpad.json` is ABI-equivalent to the current compiled +`SushiLaunchpad` artifact in the sibling contract checkout. Refresh it together +with mappings and tests whenever an indexed event changes; the E2E harness +rejects ABI drift before starting a scenario. ## Local end-to-end test @@ -141,10 +154,11 @@ LAUNCHPAD_CONTRACTS_DIR=/path/to/sushi-launchpad pnpm test:e2e -- --seed 202 ``` The default seeds are `101`, `202`, and `303`. Each scenario is fully prepared -before transactions are sent and varies actors, launch ranges, fees, canonical -token ordering, and valid action ordering while guaranteeing coverage of every -indexed lifecycle event. The first launch and factory-default changes are mined -in one block to exercise Graph Node's block-visible contract-call semantics. +before transactions are sent and varies actors, plain launches versus atomic +initial buys, fees, canonical token ordering, and valid action ordering while +guaranteeing coverage of every indexed lifecycle event. The first launch and +factory-default changes are mined in one block to exercise Graph Node's +block-visible contract-call semantics. The runner requires ports `8545`, `8000`, `8001`, `8020`, `8030`, `8040`, `5001`, and `5432`. It fails before startup if any are occupied. On success or diff --git a/subgraphs/launchpad/abis/SushiLaunchpad.json b/subgraphs/launchpad/abis/SushiLaunchpad.json index c63e3c18..b9d548c3 100644 --- a/subgraphs/launchpad/abis/SushiLaunchpad.json +++ b/subgraphs/launchpad/abis/SushiLaunchpad.json @@ -43,33 +43,98 @@ }, { "inputs": [], - "name": "EmptyRanges", + "name": "EmptySymbol", "type": "error" }, { - "inputs": [], - "name": "EmptySymbol", + "inputs": [ + { + "internalType": "uint256", + "name": "used", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "desired", + "type": "uint256" + } + ], + "name": "ExcessTokenConsumption", "type": "error" }, { "inputs": [ { "internalType": "uint256", - "name": "index", + "name": "maximum", "type": "uint256" }, { "internalType": "uint256", - "name": "used", + "name": "required", "type": "uint256" + } + ], + "name": "ExcessiveInitialBuyInput", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint80", + "name": "roundId", + "type": "uint80" + }, + { + "internalType": "uint80", + "name": "answeredInRound", + "type": "uint80" }, { "internalType": "uint256", - "name": "desired", + "name": "updatedAt", "type": "uint256" } ], - "name": "ExcessTokenConsumption", + "name": "IncompletePriceFeedRound", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "InitialBuyAmountTooLarge", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + } + ], + "name": "InitialPriceOutOfBounds", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "minimum", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "InsufficientInitialBuyOutput", "type": "error" }, { @@ -99,6 +164,33 @@ "name": "InvalidFeeBps", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "InvalidInitialBuyRecipient", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "required", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "supplied", + "type": "uint256" + } + ], + "name": "InvalidNativeValue", + "type": "error" + }, { "inputs": [], "name": "InvalidPositionManager", @@ -108,32 +200,55 @@ "inputs": [ { "internalType": "address", - "name": "quoteToken", + "name": "priceFeed", "type": "address" } ], - "name": "InvalidQuoteToken", + "name": "InvalidPriceFeed", "type": "error" }, { "inputs": [ { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, + "internalType": "int256", + "name": "answer", + "type": "int256" + } + ], + "name": "InvalidPriceFeedAnswer", + "type": "error" + }, + { + "inputs": [ { - "internalType": "int24", - "name": "startTick", - "type": "int24" - }, + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + } + ], + "name": "InvalidPriceFeedDecimals", + "type": "error" + }, + { + "inputs": [ { - "internalType": "int24", - "name": "endTick", - "type": "int24" + "internalType": "address", + "name": "quoteToken", + "type": "address" + } + ], + "name": "InvalidQuoteToken", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "decimals", + "type": "uint8" } ], - "name": "InvalidRangeOrder", + "name": "InvalidQuoteTokenDecimals", "type": "error" }, { @@ -150,44 +265,39 @@ { "inputs": [ { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, - { - "internalType": "int24", - "name": "tick", - "type": "int24" + "internalType": "address", + "name": "caller", + "type": "address" } ], - "name": "InvalidTickBounds", + "name": "InvalidSwapCallback", "type": "error" }, { "inputs": [ { - "internalType": "int24", - "name": "actualTickSpacing", - "type": "int24" + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" } ], - "name": "InvalidTickSpacing", + "name": "InvalidSwapDeltas", "type": "error" }, { "inputs": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, { "internalType": "int24", - "name": "tick", + "name": "actualTickSpacing", "type": "int24" } ], - "name": "InvalidTickSpacingAt", + "name": "InvalidTickSpacing", "type": "error" }, { @@ -213,32 +323,6 @@ }, { "inputs": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, - { - "internalType": "int24", - "name": "previousEndTick", - "type": "int24" - }, - { - "internalType": "int24", - "name": "startTick", - "type": "int24" - } - ], - "name": "NonContiguousRanges", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - }, { "internalType": "uint256", "name": "amount", @@ -280,6 +364,22 @@ "name": "OwnershipRenunciationDisabled", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "requested", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "consumed", + "type": "uint256" + } + ], + "name": "PartialInitialBuy", + "type": "error" + }, { "inputs": [ { @@ -334,33 +434,6 @@ "name": "PositionConfigurationMismatch", "type": "error" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - } - ], - "name": "RangeAmountOverflow", - "type": "error" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "supplied", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "allocation", - "type": "uint256" - } - ], - "name": "RangeAmountsExceedAllocation", - "type": "error" - }, { "inputs": [], "name": "ReentrancyGuardReentrantCall", @@ -399,6 +472,27 @@ "name": "SafeERC20FailedOperation", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "updatedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maximumAge", + "type": "uint256" + } + ], + "name": "StalePriceFeedRound", + "type": "error" + }, { "inputs": [ { @@ -415,6 +509,17 @@ "name": "SupplyReconciliationFailure", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "SwapCallbackNotConsumed", + "type": "error" + }, { "inputs": [ { @@ -429,17 +534,12 @@ { "inputs": [ { - "internalType": "uint256", - "name": "supplied", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "maximum", - "type": "uint256" + "internalType": "address", + "name": "caller", + "type": "address" } ], - "name": "TooManyRanges", + "name": "UnauthorizedCreator", "type": "error" }, { @@ -490,32 +590,56 @@ "name": "UnknownToken", "type": "error" }, + { + "inputs": [ + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + } + ], + "name": "UnsupportedQuoteToken", + "type": "error" + }, { "inputs": [], "name": "ZeroAddress", "type": "error" }, { - "inputs": [ - { - "internalType": "uint256", - "name": "index", - "type": "uint256" - } - ], + "inputs": [], + "name": "ZeroInitialBuyAmount", + "type": "error" + }, + { + "inputs": [], "name": "ZeroLiquidity", "type": "error" }, { + "anonymous": false, "inputs": [ { - "internalType": "uint256", - "name": "index", - "type": "uint256" + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "previousCreator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newCreator", + "type": "address" } ], - "name": "ZeroRangeAmount", - "type": "error" + "name": "CreatorTransferred", + "type": "event" }, { "anonymous": false, @@ -615,6 +739,55 @@ "name": "FeesDistributed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "quoteToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "name": "InitialBuyExecuted", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -809,6 +982,31 @@ "name": "ProtocolReserveWithdrawn", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "quoteToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "previousPriceFeed", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newPriceFeed", + "type": "address" + } + ], + "name": "QuoteTokenPriceFeedUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -863,27 +1061,21 @@ }, { "indexed": false, - "internalType": "string", - "name": "name", - "type": "string" + "internalType": "int24", + "name": "startTick", + "type": "int24" }, { "indexed": false, "internalType": "string", - "name": "symbol", + "name": "name", "type": "string" }, { - "indexed": false, - "internalType": "uint8", - "name": "decimals", - "type": "uint8" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "totalSupply", - "type": "uint256" + "indexed": false, + "internalType": "string", + "name": "symbol", + "type": "string" }, { "indexed": false, @@ -908,16 +1100,49 @@ "internalType": "uint16", "name": "initialSushiFeeBps", "type": "uint16" - }, + } + ], + "name": "TokenLaunched", + "type": "event" + }, + { + "inputs": [], + "name": "INITIAL_FDV_USD", + "outputs": [ { - "indexed": false, "internalType": "uint256", - "name": "positionCount", + "name": "", "type": "uint256" } ], - "name": "TokenLaunched", - "type": "event" + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_PRICE_AGE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WETH", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" }, { "inputs": [], @@ -926,6 +1151,25 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + } + ], + "name": "calculateStartTick", + "outputs": [ + { + "internalType": "int24", + "name": "startTick", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "defaultSushiFeeBps", @@ -997,35 +1241,159 @@ "name": "quoteToken", "type": "address" }, + { + "internalType": "uint64", + "name": "deadline", + "type": "uint64" + } + ], + "name": "launch", + "outputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ { "components": [ { - "internalType": "int24", - "name": "startTick", - "type": "int24" + "internalType": "string", + "name": "name", + "type": "string" }, { - "internalType": "int24", - "name": "endTick", - "type": "int24" + "internalType": "string", + "name": "symbol", + "type": "string" + } + ], + "internalType": "struct ISushiLaunchpad.TokenConfig", + "name": "tokenConfig", + "type": "tuple" + }, + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + }, + { + "internalType": "uint64", + "name": "deadline", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" }, { "internalType": "uint256", - "name": "amount", + "name": "amountOutMinimum", "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "internalType": "struct ISushiLaunchpad.InitialBuy", + "name": "initialBuy", + "type": "tuple" + } + ], + "name": "launchAndBuy", + "outputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol", + "type": "string" } ], - "internalType": "struct ISushiLaunchpad.SaleRange[]", - "name": "ranges", - "type": "tuple[]" + "internalType": "struct ISushiLaunchpad.TokenConfig", + "name": "tokenConfig", + "type": "tuple" }, { "internalType": "uint64", "name": "deadline", "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOutMinimum", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "internalType": "struct ISushiLaunchpad.InitialBuy", + "name": "initialBuy", + "type": "tuple" } ], - "name": "launch", + "name": "launchAndBuyNative", "outputs": [ { "internalType": "address", @@ -1038,9 +1406,14 @@ "type": "address" }, { - "internalType": "uint256[]", - "name": "positionIds", - "type": "uint256[]" + "internalType": "uint256", + "name": "positionId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" } ], "stateMutability": "payable", @@ -1209,6 +1582,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + } + ], + "name": "quoteTokenPriceFeed", + "outputs": [ + { + "internalType": "address", + "name": "priceFeed", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "renounceOwnership", @@ -1268,6 +1660,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "quoteToken", + "type": "address" + }, + { + "internalType": "address", + "name": "priceFeed", + "type": "address" + } + ], + "name": "setQuoteTokenPriceFeed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1305,6 +1715,24 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "newCreator", + "type": "address" + } + ], + "name": "transferCreator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1318,6 +1746,29 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "v3Factory", diff --git a/subgraphs/launchpad/e2e/model.ts b/subgraphs/launchpad/e2e/model.ts index 5d81ac8a..dab72d4c 100644 --- a/subgraphs/launchpad/e2e/model.ts +++ b/subgraphs/launchpad/e2e/model.ts @@ -10,34 +10,35 @@ export interface IndexedEvent { timestamp: number; } -export interface PoolConfiguration { - address: string; - token0: string; - token1: string; - fee: number; - tickSpacing: number; -} - export interface ExpectedModelInput { chainId: bigint; launchpadAddress: string; positionManager: string; protocolRecipient: string; launchFee: bigint; + initialFdvUsd: bigint; defaultSushiFeeBps: number; protocolReserveBps: number; + launchpadObservation: LaunchObservation | null; events: IndexedEvent[]; - pools: Map; +} + +export interface LaunchObservation { + decimals: number; + totalSupply: bigint; + poolFee: number; + poolTickSpacing: number; } type Entity = Record & { id: string }; export interface EntitySnapshot { launchpads: Entity[]; - creators: Entity[]; - tokens: Entity[]; - pools: Entity[]; - launchPositions: Entity[]; + launches: Entity[]; + pendingLaunches: Entity[]; + quoteTokenPriceFeeds: Entity[]; + creatorTransfers: Entity[]; + initialBuys: Entity[]; feeDistributions: Entity[]; reserveWithdrawals: Entity[]; launchFeeWithdrawals: Entity[]; @@ -56,16 +57,6 @@ function eventId(chainId: bigint, event: IndexedEvent): string { return `${chainId}:${event.transactionHash.toLowerCase()}:${event.logIndex}`; } -function positionId( - chainId: bigint, - positionManager: string, - onchainPositionId: unknown -): string { - return `${chainId}:${positionManager.toLowerCase()}:${decimal( - onchainPositionId - )}`; -} - function metadata(event: IndexedEvent): Record { return { transactionHash: event.transactionHash.toLowerCase(), @@ -80,18 +71,15 @@ function values(entities: Map): T[] { return [...entities.values()]; } -function add(value: unknown, amount: unknown): string { - return (BigInt(String(value)) + BigInt(amount as bigint)).toString(); -} - export function buildExpectedSnapshot( input: ExpectedModelInput ): EntitySnapshot { const launchpadId = addressId(input.chainId, input.launchpadAddress); - const creators = new Map(); - const tokens = new Map(); - const pools = new Map(); - const launchPositions = new Map(); + const launches = new Map(); + const pendingLaunches = new Map(); + const quoteTokenPriceFeeds = new Map(); + const creatorTransfers = new Map(); + const initialBuys = new Map(); const feeDistributions = new Map(); const reserveWithdrawals = new Map(); const launchFeeWithdrawals = new Map(); @@ -100,20 +88,18 @@ export function buildExpectedSnapshot( id: launchpadId, chainId: input.chainId.toString(), address: input.launchpadAddress.toLowerCase(), - addressHex: input.launchpadAddress.toLowerCase(), positionManager: input.positionManager.toLowerCase(), - positionManagerHex: input.positionManager.toLowerCase(), protocolRecipient: input.protocolRecipient.toLowerCase(), - protocolRecipientHex: input.protocolRecipient.toLowerCase(), launchFee: input.launchFee.toString(), + initialFdvUsd: input.initialFdvUsd.toString(), defaultSushiFeeBps: input.defaultSushiFeeBps, protocolReserveBps: input.protocolReserveBps, - tokenCount: 0, - creatorCount: 0, - positionCount: 0, - tokens: [], - pools: [], - creators: [], + launchTokenDecimals: null, + launchTokenTotalSupply: null, + launchPoolFee: null, + launchPoolTickSpacing: null, + launches: [], + quoteTokenPriceFeeds: [], launchFeeWithdrawals: [], }; @@ -125,297 +111,280 @@ export function buildExpectedSnapshot( for (const event of orderedEvents) { const args = event.args; - if (event.name === "PositionCreated") { - const poolAddress = lower(args.pool); - const poolConfiguration = input.pools.get(poolAddress); - assert( - poolConfiguration, - `Missing pool configuration for ${poolAddress}` - ); - const idForPool = addressId(input.chainId, poolAddress); - let pool = pools.get(idForPool); - if (!pool) { - const launchedToken = lower(args.token); - const tokenIs0 = - poolConfiguration.token0.toLowerCase() === launchedToken; - const tokenIs1 = - poolConfiguration.token1.toLowerCase() === launchedToken; + if (event.name === "TokenLaunched") { + const id = addressId(input.chainId, args.token); + const token = lower(args.token); + const quoteToken = lower(args.quoteToken); + if (launchpad.launchTokenDecimals === null) { + const observation = input.launchpadObservation; assert( - tokenIs0 !== tokenIs1, - `Launch token is not exactly one side of pool ${poolAddress}` + observation, + `Missing observed launchpad values for ${launchpadId}` ); - pool = { - id: idForPool, - chainId: input.chainId.toString(), - address: poolAddress, - addressHex: poolAddress, - launchpad: relation(launchpadId), - token0: poolConfiguration.token0.toLowerCase(), - token0Hex: poolConfiguration.token0.toLowerCase(), - token1: poolConfiguration.token1.toLowerCase(), - token1Hex: poolConfiguration.token1.toLowerCase(), - fee: poolConfiguration.fee, - tickSpacing: poolConfiguration.tickSpacing, - positionManager: input.positionManager.toLowerCase(), - positionManagerHex: input.positionManager.toLowerCase(), - positionCount: 0, - creationTransactionHash: event.transactionHash.toLowerCase(), - creationTransactionHashHex: event.transactionHash.toLowerCase(), - creationBlockNumber: event.blockNumber.toString(), - creationBlockHash: event.blockHash.toLowerCase(), - creationBlockHashHex: event.blockHash.toLowerCase(), - createdAt: event.timestamp.toString(), - positions: [], - }; - pools.set(idForPool, pool); + launchpad.launchTokenDecimals = observation.decimals; + launchpad.launchTokenTotalSupply = observation.totalSupply.toString(); + launchpad.launchPoolFee = observation.poolFee; + launchpad.launchPoolTickSpacing = observation.poolTickSpacing; } - - const tokenIs0 = - poolConfiguration.token0.toLowerCase() === lower(args.token); - const idForPosition = positionId( - input.chainId, - input.positionManager, - args.positionId - ); - const index = pool.positionCount as number; - const zero = "0"; - const desired = decimal(args.tokenDesired); - const used = decimal(args.tokenUsed); - const position: Entity = { - id: idForPosition, + assert(!pendingLaunches.has(id) && !launches.has(id)); + pendingLaunches.set(id, { + id, chainId: input.chainId.toString(), - positionManager: input.positionManager.toLowerCase(), - positionManagerHex: input.positionManager.toLowerCase(), - positionId: decimal(args.positionId), - index, - pool: relation(idForPool), - tickLower: integer(args.tickLower), - tickUpper: integer(args.tickUpper), - liquidity: decimal(args.liquidity), - amount0Desired: tokenIs0 ? desired : zero, - amount1Desired: tokenIs0 ? zero : desired, - amount0: tokenIs0 ? used : zero, - amount1: tokenIs0 ? zero : used, + launchpad: relation(launchpadId), + token, + creator: lower(args.creator), + quoteToken, + pool: lower(args.pool), + launchTokenIsToken0: token < quoteToken, + name: String(args.name), + symbol: String(args.symbol), + startTick: integer(args.startTick), + sushiFeeBps: integer(args.initialSushiFeeBps), + reserveBps: integer(args.reserveBps), + reserveAmount: decimal(args.reserveAmount), + reserveUnlockAt: decimal(args.reserveUnlockAt), creationTransactionHash: event.transactionHash.toLowerCase(), - creationTransactionHashHex: event.transactionHash.toLowerCase(), creationLogIndex: event.logIndex.toString(), creationBlockNumber: event.blockNumber.toString(), creationBlockHash: event.blockHash.toLowerCase(), - creationBlockHashHex: event.blockHash.toLowerCase(), createdAt: event.timestamp.toString(), - }; - launchPositions.set(idForPosition, position); - (pool.positions as Array<{ id: string }>).push(relation(idForPosition)); - pool.positionCount = index + 1; - launchpad.positionCount = (launchpad.positionCount as number) + 1; + }); continue; } - if (event.name === "TokenLaunched") { - const idForToken = addressId(input.chainId, args.token); - const idForCreator = `${launchpadId}:${lower(args.creator)}`; - const idForPool = addressId(input.chainId, args.pool); - const pool = pools.get(idForPool); - assert(pool, `Missing launch pool ${idForPool}`); - const tokenAddress = lower(args.token); - const quoteToken = lower(args.quoteToken); - assert( - (lower(pool.token0) === tokenAddress && - lower(pool.token1) === quoteToken) || - (lower(pool.token1) === tokenAddress && - lower(pool.token0) === quoteToken), - `TokenLaunched pair does not match pool ${idForPool}` - ); + if (event.name === "PositionCreated") { + const id = addressId(input.chainId, args.token); + const pending = pendingLaunches.get(id); + assert(pending, `Missing pending launch ${id}`); + assert.equal(pending.pool, lower(args.pool)); assert.equal( - pool.positionCount, - integer(args.positionCount), - `TokenLaunched position count does not match pool ${idForPool}` + pending.creationTransactionHash, + event.transactionHash.toLowerCase() ); - let creator = creators.get(idForCreator); - if (!creator) { - creator = { - id: idForCreator, - chainId: input.chainId.toString(), - address: lower(args.creator), - addressHex: lower(args.creator), - launchpad: relation(launchpadId), - tokenCount: 0, - tokens: [], - }; - creators.set(idForCreator, creator); - launchpad.creatorCount = (launchpad.creatorCount as number) + 1; - } - creator.tokenCount = (creator.tokenCount as number) + 1; - (creator.tokens as Array<{ id: string }>).push(relation(idForToken)); - - const token: Entity = { - id: idForToken, + const launch: Entity = { + id, chainId: input.chainId.toString(), - address: lower(args.token), - addressHex: lower(args.token), launchpad: relation(launchpadId), - creator: relation(idForCreator), - pool: relation(idForPool), - quoteToken, - quoteTokenHex: quoteToken, - name: String(args.name), - symbol: String(args.symbol), - decimals: integer(args.decimals), - totalSupply: decimal(args.totalSupply), - sushiFeeBps: integer(args.initialSushiFeeBps), - reserveBps: integer(args.reserveBps), - reserveAmount: decimal(args.reserveAmount), - reserveUnlockAt: decimal(args.reserveUnlockAt), + token: pending.token, + initialCreator: pending.creator, + creator: pending.creator, + quoteToken: pending.quoteToken, + pool: pending.pool, + launchTokenIsToken0: pending.launchTokenIsToken0, + name: pending.name, + symbol: pending.symbol, + decimals: launchpad.launchTokenDecimals, + totalSupply: launchpad.launchTokenTotalSupply, + initialFdvUsd: launchpad.initialFdvUsd, + startTick: pending.startTick, + poolFee: launchpad.launchPoolFee, + poolTickSpacing: launchpad.launchPoolTickSpacing, + positionManager: input.positionManager.toLowerCase(), + positionId: decimal(args.positionId), + tickLower: integer(args.tickLower), + tickUpper: integer(args.tickUpper), + tokenDesired: decimal(args.tokenDesired), + tokenUsed: decimal(args.tokenUsed), + liquidity: decimal(args.liquidity), + sushiFeeBps: pending.sushiFeeBps, + reserveBps: pending.reserveBps, + reserveAmount: pending.reserveAmount, + reserveUnlockAt: pending.reserveUnlockAt, reserveWithdrawn: false, reserveWithdrawal: null, - totalAmount0Collected: "0", - totalAmount1Collected: "0", - totalAmount0ToSushi: "0", - totalAmount1ToSushi: "0", - totalAmount0ToCreator: "0", - totalAmount1ToCreator: "0", + initialBuy: null, feeDistributions: [], - creationTransactionHash: event.transactionHash.toLowerCase(), - creationTransactionHashHex: event.transactionHash.toLowerCase(), - creationLogIndex: event.logIndex.toString(), - creationBlockNumber: event.blockNumber.toString(), - creationBlockHash: event.blockHash.toLowerCase(), - creationBlockHashHex: event.blockHash.toLowerCase(), - createdAt: event.timestamp.toString(), + creatorTransfers: [], + creationTransactionHash: pending.creationTransactionHash, + creationLogIndex: pending.creationLogIndex, + positionCreationLogIndex: event.logIndex.toString(), + creationBlockNumber: pending.creationBlockNumber, + creationBlockHash: pending.creationBlockHash, + createdAt: pending.createdAt, }; - tokens.set(idForToken, token); - launchpad.tokenCount = (launchpad.tokenCount as number) + 1; + pendingLaunches.delete(id); + launches.set(id, launch); + continue; + } + + if (event.name === "InitialBuyExecuted") { + const launch = launches.get(addressId(input.chainId, args.token)); + assert(launch, `Unknown initial buy launch ${lower(args.token)}`); + const id = eventId(input.chainId, event); + initialBuys.set(id, { + id, + chainId: input.chainId.toString(), + launch: relation(launch.id), + creator: lower(args.creator), + recipient: lower(args.recipient), + quoteToken: lower(args.quoteToken), + pool: lower(args.pool), + amountIn: decimal(args.amountIn), + amountOut: decimal(args.amountOut), + ...metadata(event), + }); + launch.initialBuy = relation(id); + continue; + } + + if (event.name === "CreatorTransferred") { + const launch = launches.get(addressId(input.chainId, args.token)); + assert(launch, `Unknown creator transfer launch ${lower(args.token)}`); + assert.equal(launch.creator, lower(args.previousCreator)); + const id = eventId(input.chainId, event); + creatorTransfers.set(id, { + id, + chainId: input.chainId.toString(), + launch: relation(launch.id), + previousCreator: lower(args.previousCreator), + newCreator: lower(args.newCreator), + ...metadata(event), + }); + launch.creator = lower(args.newCreator); + (launch.creatorTransfers as Array<{ id: string }>).push(relation(id)); continue; } if (event.name === "SushiFeeBpsUpdated") { - const token = tokens.get(addressId(input.chainId, args.token)); - assert( - token, - `Unknown token in SushiFeeBpsUpdated: ${lower(args.token)}` - ); - token.sushiFeeBps = integer(args.newSushiFeeBps); + const launch = launches.get(addressId(input.chainId, args.token)); + assert(launch, `Unknown fee update launch ${lower(args.token)}`); + launch.sushiFeeBps = integer(args.newSushiFeeBps); continue; } if (event.name === "FeesDistributed") { - const idForToken = addressId(input.chainId, args.token); - const idForPool = addressId(input.chainId, args.pool); - const token = tokens.get(idForToken); - const pool = pools.get(idForPool); - assert(token && pool, `Unknown fee distribution target ${idForToken}`); + const launch = launches.get(addressId(input.chainId, args.token)); + assert(launch, `Unknown fee distribution launch ${lower(args.token)}`); const id = eventId(input.chainId, event); - const tokenIs0 = lower(pool.token0) === lower(args.token); - const amount0Collected = tokenIs0 - ? decimal(args.tokenCollected) - : decimal(args.quoteCollected); - const amount1Collected = tokenIs0 - ? decimal(args.quoteCollected) - : decimal(args.tokenCollected); - const amount0ToSushi = tokenIs0 - ? decimal(args.tokenToSushi) - : decimal(args.quoteToSushi); - const amount1ToSushi = tokenIs0 - ? decimal(args.quoteToSushi) - : decimal(args.tokenToSushi); - const amount0ToCreator = tokenIs0 - ? decimal(args.tokenToCreator) - : decimal(args.quoteToCreator); - const amount1ToCreator = tokenIs0 - ? decimal(args.quoteToCreator) - : decimal(args.tokenToCreator); - const distribution: Entity = { + const tokenIs0 = Boolean(launch.launchTokenIsToken0); + feeDistributions.set(id, { id, chainId: input.chainId.toString(), - token: relation(idForToken), - pool: relation(idForPool), + launch: relation(launch.id), + pool: lower(args.pool), caller: lower(args.caller), sushiRecipient: lower(args.protocolRecipient), creatorRecipient: lower(args.creator), sushiFeeBps: integer(args.sushiFeeBps), - amount0Collected, - amount1Collected, - amount0ToSushi, - amount1ToSushi, - amount0ToCreator, - amount1ToCreator, + amount0Collected: decimal( + tokenIs0 ? args.tokenCollected : args.quoteCollected + ), + amount1Collected: decimal( + tokenIs0 ? args.quoteCollected : args.tokenCollected + ), + amount0ToSushi: decimal( + tokenIs0 ? args.tokenToSushi : args.quoteToSushi + ), + amount1ToSushi: decimal( + tokenIs0 ? args.quoteToSushi : args.tokenToSushi + ), + amount0ToCreator: decimal( + tokenIs0 ? args.tokenToCreator : args.quoteToCreator + ), + amount1ToCreator: decimal( + tokenIs0 ? args.quoteToCreator : args.tokenToCreator + ), ...metadata(event), - }; - feeDistributions.set(id, distribution); - (token.feeDistributions as Array<{ id: string }>).push(relation(id)); - token.totalAmount0Collected = add( - token.totalAmount0Collected, - amount0Collected - ); - token.totalAmount1Collected = add( - token.totalAmount1Collected, - amount1Collected - ); - token.totalAmount0ToSushi = add( - token.totalAmount0ToSushi, - amount0ToSushi - ); - token.totalAmount1ToSushi = add( - token.totalAmount1ToSushi, - amount1ToSushi - ); - token.totalAmount0ToCreator = add( - token.totalAmount0ToCreator, - amount0ToCreator - ); - token.totalAmount1ToCreator = add( - token.totalAmount1ToCreator, - amount1ToCreator - ); + }); + (launch.feeDistributions as Array<{ id: string }>).push(relation(id)); continue; } if (event.name === "ProtocolReserveWithdrawn") { - const idForToken = addressId(input.chainId, args.token); - const token = tokens.get(idForToken); - assert(token, `Unknown reserve withdrawal target ${idForToken}`); + const launch = launches.get(addressId(input.chainId, args.token)); + assert(launch, `Unknown reserve withdrawal launch ${lower(args.token)}`); const id = eventId(input.chainId, event); - const withdrawal: Entity = { + reserveWithdrawals.set(id, { id, chainId: input.chainId.toString(), - token: relation(idForToken), + launch: relation(launch.id), recipient: lower(args.recipient), amount: decimal(args.amount), ...metadata(event), - }; - reserveWithdrawals.set(id, withdrawal); - token.reserveWithdrawn = true; - token.reserveWithdrawal = relation(id); + }); + launch.reserveWithdrawn = true; + launch.reserveWithdrawal = relation(id); + continue; + } + + if (event.name === "DefaultSushiFeeBpsUpdated") { + launchpad.defaultSushiFeeBps = integer(args.newBps); + continue; + } + if (event.name === "ProtocolReserveBpsUpdated") { + launchpad.protocolReserveBps = integer(args.newBps); + continue; + } + if (event.name === "ProtocolRecipientUpdated") { + launchpad.protocolRecipient = lower(args.newRecipient); + continue; + } + if (event.name === "LaunchFeeUpdated") { + launchpad.launchFee = decimal(args.newFee); + continue; + } + + if (event.name === "QuoteTokenPriceFeedUpdated") { + const id = `${launchpadId}:${lower(args.quoteToken)}`; + const current = quoteTokenPriceFeeds.get(id); + if (current) { + assert.equal(current.priceFeed, lower(args.previousPriceFeed)); + } else { + assert.equal( + lower(args.previousPriceFeed), + "0x0000000000000000000000000000000000000000" + ); + } + if ( + lower(args.newPriceFeed) === + "0x0000000000000000000000000000000000000000" + ) { + quoteTokenPriceFeeds.delete(id); + continue; + } + quoteTokenPriceFeeds.set(id, { + id, + chainId: input.chainId.toString(), + launchpad: relation(launchpadId), + quoteToken: lower(args.quoteToken), + priceFeed: lower(args.newPriceFeed), + updatedTransactionHash: event.transactionHash.toLowerCase(), + updatedLogIndex: event.logIndex.toString(), + updatedBlockNumber: event.blockNumber.toString(), + updatedBlockHash: event.blockHash.toLowerCase(), + updatedAt: event.timestamp.toString(), + }); continue; } if (event.name === "LaunchFeesWithdrawn") { const id = eventId(input.chainId, event); - const withdrawal: Entity = { + launchFeeWithdrawals.set(id, { id, chainId: input.chainId.toString(), launchpad: relation(launchpadId), recipient: lower(args.recipient), amount: decimal(args.amount), ...metadata(event), - }; - launchFeeWithdrawals.set(id, withdrawal); + }); } } - launchpad.tokens = values(tokens).map((entity) => relation(entity.id)); - launchpad.pools = values(pools).map((entity) => relation(entity.id)); - launchpad.creators = values(creators).map((entity) => relation(entity.id)); + launchpad.launches = values(launches).map((entity) => relation(entity.id)); + launchpad.quoteTokenPriceFeeds = values(quoteTokenPriceFeeds).map((entity) => + relation(entity.id) + ); launchpad.launchFeeWithdrawals = values(launchFeeWithdrawals).map((entity) => relation(entity.id) ); return normalizeSnapshot({ launchpads: [launchpad], - creators: values(creators), - tokens: values(tokens), - pools: values(pools), - launchPositions: values(launchPositions), + launches: values(launches), + pendingLaunches: values(pendingLaunches), + quoteTokenPriceFeeds: values(quoteTokenPriceFeeds), + creatorTransfers: values(creatorTransfers), + initialBuys: values(initialBuys), feeDistributions: values(feeDistributions), reserveWithdrawals: values(reserveWithdrawals), launchFeeWithdrawals: values(launchFeeWithdrawals), diff --git a/subgraphs/launchpad/e2e/run.ts b/subgraphs/launchpad/e2e/run.ts index 9f19ba47..e39ded13 100644 --- a/subgraphs/launchpad/e2e/run.ts +++ b/subgraphs/launchpad/e2e/run.ts @@ -18,7 +18,7 @@ import { normalizeSnapshot, type EntitySnapshot, type IndexedEvent, - type PoolConfiguration, + type LaunchObservation, } from "./model"; import { CHAIN_ID, @@ -56,7 +56,9 @@ interface ContractArtifacts { launchpad: HardhatArtifact; launchToken: HardhatArtifact; quoteToken: HardhatArtifact; + priceFeed: HardhatArtifact; factory: HardhatArtifact; + weth: HardhatArtifact; positionManager: HardhatArtifact; pool: HardhatArtifact; } @@ -78,13 +80,14 @@ interface LaunchedToken { address: string; pool: string; quoteToken: string; + creatorAccount: number; positionIds: bigint[]; token: Contract; } interface ScenarioExecution { events: IndexedEvent[]; - pools: Map; + launchpadObservation: LaunchObservation | null; finalBlock: number; } @@ -364,9 +367,13 @@ async function loadArtifacts( quoteToken: await readArtifact( artifact("test/MockSushiV3.sol", "MockERC20.json") ), + priceFeed: await readArtifact( + artifact("test/MockSushiV3.sol", "MockChainlinkAggregator.json") + ), factory: await readArtifact( artifact("test/MockSushiV3.sol", "MockSushiV3Factory.json") ), + weth: await readArtifact(artifact("test/MockSushiV3.sol", "MockWETH.json")), positionManager: await readArtifact( artifact("test/MockSushiV3.sol", "MockPositionManager.json") ), @@ -440,9 +447,10 @@ async function deployFixture( ); } const factory = await deploy(artifacts.factory, owner); + const weth = await deploy(artifacts.weth, owner); const positionManager = await deploy(artifacts.positionManager, owner, [ await factory.getAddress(), - LOW_QUOTE_TOKEN_ADDRESS, + await weth.getAddress(), ]); await (await positionManager.setConsumptionBps(plan.consumptionBps)).wait(); const launchpad = await deploy(artifacts.launchpad, owner, [ @@ -453,6 +461,18 @@ async function deployFixture( ]); const deploymentReceipt = await launchpad.deploymentTransaction()!.wait(); assert(deploymentReceipt, "Launchpad deployment did not produce a receipt"); + for (const address of [LOW_QUOTE_TOKEN_ADDRESS, HIGH_QUOTE_TOKEN_ADDRESS]) { + const priceFeed = await deploy(artifacts.priceFeed, owner, [ + 8, + 100_000_000n, + ]); + await ( + await launchpad.setQuoteTokenPriceFeed( + address, + await priceFeed.getAddress() + ) + ).wait(); + } return { launchpad, @@ -552,12 +572,45 @@ async function executeScenario( plan: ScenarioPlan ): Promise { const events: IndexedEvent[] = []; + let launchpadObservation: LaunchObservation | null = null; const tokens = new Map(); const blockTimestamps = new Map(); const launchpadInterface = new Interface( fixture.launchpad.interface.fragments ); + // The data source starts at deployment, before scenario execution. Include + // the quote-token feed configuration logs emitted while preparing the + // fixture so the independent expected model covers the full indexed range. + const setupLogs = await provider.getLogs({ + address: fixture.launchpadAddress, + fromBlock: fixture.launchpadDeploymentBlock, + toBlock: "latest", + }); + for (const log of setupLogs) { + const parsed = launchpadInterface.parseLog({ + topics: [...log.topics], + data: log.data, + }); + if (!parsed) continue; + let timestamp = blockTimestamps.get(log.blockNumber); + if (timestamp === undefined) { + const block = await provider.getBlock(log.blockNumber); + assert(block, `Missing block ${log.blockNumber}`); + timestamp = block.timestamp; + blockTimestamps.set(log.blockNumber, timestamp); + } + events.push({ + name: parsed.name, + args: eventArguments(parsed), + transactionHash: log.transactionHash.toLowerCase(), + logIndex: log.index, + blockNumber: log.blockNumber, + blockHash: log.blockHash.toLowerCase(), + timestamp, + }); + } + const recordReceipt = async ( transaction: TransactionResponse ): Promise => { @@ -605,11 +658,38 @@ async function executeScenario( const connected = fixture.launchpad.connect( signers[action.creatorAccount]! ) as Contract; - return await connected.launch( + const deadline = BigInt(block.timestamp + 3_600); + if (action.initialBuyAmount === 0n) { + return await connected.launch( + { name: action.name, symbol: action.symbol }, + action.quoteToken, + deadline, + { value: action.launchFee } + ); + } + const quoteToken = fixture.quoteTokens.get(action.quoteToken); + assert(quoteToken, `Unknown quote token ${action.quoteToken}`); + const creator = signers[action.creatorAccount]!; + const creatorAddress = await creator.getAddress(); + await ( + await quoteToken.mint(creatorAddress, action.initialBuyAmount) + ).wait(); + const creatorQuoteToken = quoteToken.connect(creator) as Contract; + await ( + await creatorQuoteToken.approve( + fixture.launchpadAddress, + action.initialBuyAmount + ) + ).wait(); + return await connected.launchAndBuy( { name: action.name, symbol: action.symbol }, action.quoteToken, - action.ranges, - BigInt(block.timestamp + 3_600), + deadline, + { + amountIn: action.initialBuyAmount, + amountOutMinimum: 0, + recipient: creatorAddress, + }, { value: action.launchFee } ); }; @@ -647,19 +727,49 @@ async function executeScenario( ); assert.equal( positionEvents.length, - action.ranges.length, + 1, `Unexpected position count for ${action.tokenKey}` ); + const launchpadEvents = events.filter( + (event) => event.transactionHash === receipt.hash.toLowerCase() + ); + const launchIndex = launchpadEvents.findIndex( + (event) => event.name === "TokenLaunched" + ); + const positionIndex = launchpadEvents.findIndex( + (event) => event.name === "PositionCreated" + ); + const buyIndex = launchpadEvents.findIndex( + (event) => event.name === "InitialBuyExecuted" + ); + assert(launchIndex >= 0 && positionIndex > launchIndex); + assert.equal(buyIndex > positionIndex, action.initialBuyAmount > 0n); const address = String(launchEvent.args.token).toLowerCase(); + const poolAddress = String(launchEvent.args.pool).toLowerCase(); + const token = new Contract(address, fixture.tokenArtifact.abi, signers[0]); + if (launchpadObservation === null) { + const pool = new Contract( + poolAddress, + fixture.poolArtifact.abi, + signers[0] + ); + launchpadObservation = { + decimals: Number(await token.decimals()), + totalSupply: BigInt(await token.totalSupply()), + poolFee: Number(await pool.fee()), + poolTickSpacing: Number(await pool.tickSpacing()), + }; + } tokens.set(action.tokenKey, { tokenKey: action.tokenKey, address, - pool: String(launchEvent.args.pool).toLowerCase(), + pool: poolAddress, quoteToken, + creatorAccount: action.creatorAccount, positionIds: positionEvents.map((event) => BigInt(event.args.positionId as bigint) ), - token: new Contract(address, fixture.tokenArtifact.abi, signers[0]), + token, }); }; @@ -695,6 +805,21 @@ async function executeScenario( await recordReceipt(await fixture.launchpad.setLaunchFee(action.value)); return; } + if (action.kind === "transferCreator") { + const token = tokens.get(action.tokenKey); + assert(token, `Unknown planned token ${action.tokenKey}`); + const connected = fixture.launchpad.connect( + signers[token.creatorAccount]! + ) as Contract; + await recordReceipt( + await connected.transferCreator( + token.address, + await signers[action.newCreatorAccount]!.getAddress() + ) + ); + token.creatorAccount = action.newCreatorAccount; + return; + } if (action.kind === "setTokenSushiFeeBps") { const token = tokens.get(action.tokenKey); assert(token, `Unknown planned token ${action.tokenKey}`); @@ -716,15 +841,10 @@ async function executeScenario( token.address.toLowerCase(); const quoteToken = fixture.quoteTokens.get(token.quoteToken); assert(quoteToken, `Unknown quote token ${token.quoteToken}`); - await ( - await quoteToken.mint( - fixture.positionManagerAddress, - action.quoteAmount - ) - ).wait(); + await (await quoteToken.mint(token.pool, action.quoteAmount)).wait(); await ( await fixture.positionManager.seedFees( - token.positionIds[action.positionIndex], + token.positionIds[0], tokenIs0 ? action.tokenAmount : action.quoteAmount, tokenIs0 ? action.quoteAmount : action.tokenAmount ) @@ -788,26 +908,9 @@ async function executeScenario( } } - const pools = new Map(); - for (const token of tokens.values()) { - if (pools.has(token.pool)) continue; - const contract = new Contract( - token.pool, - fixture.poolArtifact.abi, - signers[0] - ); - pools.set(token.pool, { - address: token.pool, - token0: String(await contract.token0()).toLowerCase(), - token1: String(await contract.token1()).toLowerCase(), - fee: Number(await contract.fee()), - tickSpacing: Number(await contract.tickSpacing()), - }); - } - return { events, - pools, + launchpadObservation, finalBlock: await provider.getBlockNumber(), }; } @@ -858,51 +961,59 @@ async function waitForIndexedHead( const snapshotQuery = ` query E2ESnapshot { launchpads(first: 1000, orderBy: id) { - id chainId address addressHex positionManager positionManagerHex - protocolRecipient protocolRecipientHex launchFee - defaultSushiFeeBps protocolReserveBps tokenCount creatorCount positionCount - tokens { id } pools { id } creators { id } launchFeeWithdrawals { id } + id chainId address positionManager protocolRecipient launchFee initialFdvUsd + defaultSushiFeeBps protocolReserveBps + launchTokenDecimals launchTokenTotalSupply launchPoolFee + launchPoolTickSpacing + launches { id } quoteTokenPriceFeeds { id } launchFeeWithdrawals { id } + } + launches(first: 1000, orderBy: id) { + id chainId launchpad { id } + token initialCreator creator quoteToken pool launchTokenIsToken0 + name symbol decimals totalSupply initialFdvUsd startTick + poolFee poolTickSpacing positionManager positionId + tickLower tickUpper tokenDesired tokenUsed liquidity sushiFeeBps + reserveBps reserveAmount reserveUnlockAt reserveWithdrawn + reserveWithdrawal { id } initialBuy { id } + feeDistributions { id } creatorTransfers { id } + creationTransactionHash creationLogIndex + positionCreationLogIndex creationBlockNumber creationBlockHash + createdAt } - creators(first: 1000, orderBy: id) { - id chainId address addressHex launchpad { id } tokenCount tokens { id } + pendingLaunches(first: 1000, orderBy: id) { + id chainId launchpad { id } + token creator quoteToken pool launchTokenIsToken0 + name symbol startTick + sushiFeeBps reserveBps reserveAmount reserveUnlockAt + creationTransactionHash creationLogIndex creationBlockNumber + creationBlockHash createdAt } - tokens(first: 1000, orderBy: id) { - id chainId address addressHex launchpad { id } creator { id } pool { id } - quoteToken quoteTokenHex name symbol decimals totalSupply sushiFeeBps - reserveBps reserveAmount - reserveUnlockAt reserveWithdrawn reserveWithdrawal { id } - totalAmount0Collected totalAmount1Collected totalAmount0ToSushi - totalAmount1ToSushi totalAmount0ToCreator totalAmount1ToCreator - feeDistributions { id } - creationTransactionHash creationTransactionHashHex creationLogIndex - creationBlockNumber creationBlockHash creationBlockHashHex createdAt + quoteTokenPriceFeeds(first: 1000, orderBy: id) { + id chainId launchpad { id } quoteToken priceFeed + updatedTransactionHash updatedLogIndex updatedBlockNumber updatedBlockHash + updatedAt } - pools(first: 1000, orderBy: id) { - id chainId address addressHex launchpad { id } positions { id } - token0 token0Hex token1 token1Hex fee tickSpacing - positionManager positionManagerHex positionCount - creationTransactionHash creationTransactionHashHex creationBlockNumber - creationBlockHash creationBlockHashHex createdAt + creatorTransfers(first: 1000, orderBy: id) { + id chainId launch { id } previousCreator newCreator transactionHash + logIndex blockNumber blockHash timestamp } - launchPositions(first: 1000, orderBy: id) { - id chainId positionManager positionManagerHex positionId index pool { id } - tickLower tickUpper liquidity amount0Desired amount1Desired amount0 amount1 - creationTransactionHash creationTransactionHashHex creationLogIndex - creationBlockNumber creationBlockHash creationBlockHashHex createdAt + initialBuys(first: 1000, orderBy: id) { + id chainId launch { id } creator recipient quoteToken pool + amountIn amountOut transactionHash logIndex blockNumber blockHash timestamp } feeDistributions(first: 1000, orderBy: id) { - id chainId token { id } pool { id } caller sushiRecipient creatorRecipient - sushiFeeBps amount0Collected amount1Collected amount0ToSushi amount1ToSushi - amount0ToCreator amount1ToCreator transactionHash logIndex blockNumber - blockHash timestamp + id chainId launch { id } pool caller sushiRecipient creatorRecipient + sushiFeeBps amount0Collected amount1Collected amount0ToSushi + amount1ToSushi amount0ToCreator amount1ToCreator transactionHash + logIndex blockNumber blockHash timestamp } reserveWithdrawals(first: 1000, orderBy: id) { - id chainId token { id } recipient amount transactionHash logIndex - blockNumber blockHash timestamp + id chainId launch { id } recipient amount transactionHash + logIndex blockNumber blockHash timestamp } launchFeeWithdrawals(first: 1000, orderBy: id) { - id chainId launchpad { id } recipient amount transactionHash logIndex - blockNumber blockHash timestamp + id chainId launchpad { id } recipient amount transactionHash + logIndex blockNumber blockHash timestamp } } `; @@ -948,10 +1059,11 @@ async function runSeed( positionManager: fixture.positionManagerAddress, protocolRecipient: String(await fixture.launchpad.protocolRecipient()), launchFee: BigInt(await fixture.launchpad.launchFee()), + initialFdvUsd: BigInt(await fixture.launchpad.INITIAL_FDV_USD()), defaultSushiFeeBps: Number(await fixture.launchpad.defaultSushiFeeBps()), protocolReserveBps: Number(await fixture.launchpad.protocolReserveBps()), + launchpadObservation: execution.launchpadObservation, events: execution.events, - pools: execution.pools, }); await writeFile( path.join(seedDirectory, "graphql.json"), diff --git a/subgraphs/launchpad/e2e/scenario.ts b/subgraphs/launchpad/e2e/scenario.ts index 532a5a86..5bb521bc 100644 --- a/subgraphs/launchpad/e2e/scenario.ts +++ b/subgraphs/launchpad/e2e/scenario.ts @@ -2,7 +2,6 @@ export const DEFAULT_SEEDS = [101, 202, 303] as const; export const CHAIN_ID = 31_337n; export const UNIT = 10n ** 18n; -export const TOKEN_SUPPLY = 1_000_000_000n * UNIT; export const INITIAL_LAUNCH_FEE = 500_000_000_000_000n; export const INITIAL_SUSHI_FEE_BPS = 7_000; export const INITIAL_RESERVE_BPS = 300; @@ -13,12 +12,6 @@ export const LOW_QUOTE_TOKEN_ADDRESS = export const HIGH_QUOTE_TOKEN_ADDRESS = "0xfffffffffffffffffffffffffffffffffffffffe"; -export interface SaleRangePlan { - startTick: number; - endTick: number; - amount: bigint; -} - export interface LaunchPlan { kind: "launch"; tokenKey: string; @@ -26,7 +19,7 @@ export interface LaunchPlan { quoteToken: string; name: string; symbol: string; - ranges: SaleRangePlan[]; + initialBuyAmount: bigint; launchFee: bigint; } @@ -44,12 +37,12 @@ export type ScenarioAction = | { kind: "setProtocolReserveBps"; value: number } | { kind: "setProtocolRecipient"; account: number } | { kind: "setLaunchFee"; value: bigint } + | { kind: "transferCreator"; tokenKey: string; newCreatorAccount: number } | { kind: "setTokenSushiFeeBps"; tokenKey: string; value: number } | { kind: "distributeFees"; tokenKey: string; callerAccount: number; - positionIndex: number; quoteAmount: bigint; tokenAmount: bigint; } @@ -108,18 +101,6 @@ function launchPlan( index: number, creatorAccount: number ): LaunchPlan { - const rangeCount = random.int(1, 4); - const firstTick = -rangeCount * 200; - const ranges: SaleRangePlan[] = []; - - for (let rangeIndex = 0; rangeIndex < rangeCount; rangeIndex += 1) { - ranges.push({ - startTick: firstTick + rangeIndex * 200, - endTick: firstTick + (rangeIndex + 1) * 200, - amount: BigInt(random.int(80, 140)) * 1_000_000n * UNIT, - }); - } - return { kind: "launch", tokenKey: `token-${index}`, @@ -128,7 +109,7 @@ function launchPlan( index % 2 === 0 ? HIGH_QUOTE_TOKEN_ADDRESS : LOW_QUOTE_TOKEN_ADDRESS, name: `Launchpad Agent ${index + 1}`, symbol: `LPA${index + 1}`, - ranges, + initialBuyAmount: index % 2 === 1 ? BigInt(random.int(1, 5)) * UNIT : 0n, launchFee: INITIAL_LAUNCH_FEE, }; } @@ -136,14 +117,12 @@ function launchPlan( function distribution( random: SeededRandom, tokenKey: string, - callerAccount: number, - maxPositionIndex: number + callerAccount: number ): ScenarioAction { return { kind: "distributeFees", tokenKey, callerAccount, - positionIndex: random.int(0, maxPositionIndex), quoteAmount: BigInt(random.int(51, 500)), tokenAmount: BigInt(random.int(51, 500)), }; @@ -213,6 +192,13 @@ export function planScenario(seed: number): ScenarioPlan { { kind: "setDefaultSushiFeeBps", value: finalDefault }, { kind: "setProtocolReserveBps", value: finalReserve } ); + actions.push({ + kind: "transferCreator", + tokenKey: firstLaunch.tokenKey, + newCreatorAccount: [3, 4, 5].find( + (account) => account !== firstLaunch.creatorAccount + )!, + }); const firstTokenFee = random.pick([1_234, 3_333, 5_000, 8_765]); const secondTokenFee = random.pick([777, 2_222, 6_666, 9_999]); @@ -225,8 +211,7 @@ export function planScenario(seed: number): ScenarioPlan { distribution( random, firstLaunch.tokenKey, - random.pick([6, 7]), - firstLaunch.ranges.length - 1 + random.pick([6, 7]) ) ); actions.push({ kind: "setProtocolRecipient", account: 2 }); @@ -234,8 +219,7 @@ export function planScenario(seed: number): ScenarioPlan { distribution( random, firstLaunch.tokenKey, - random.pick([6, 7]), - firstLaunch.ranges.length - 1 + random.pick([6, 7]) ) ); actions.push({ @@ -247,8 +231,7 @@ export function planScenario(seed: number): ScenarioPlan { distribution( random, firstRemaining.tokenKey, - random.pick([6, 7]), - firstRemaining.ranges.length - 1 + random.pick([6, 7]) ) ); @@ -312,27 +295,13 @@ export function validateScenario(plan: ScenarioPlan): void { } for (const launch of launches) { - if (launch.ranges.length < 1 || launch.ranges.length > 16) { - throw new Error(`Invalid range count for ${launch.tokenKey}`); - } - let amountSum = 0n; - for (let index = 0; index < launch.ranges.length; index += 1) { - const range = launch.ranges[index]!; - if ( - range.amount <= 0n || - range.startTick >= range.endTick || - range.startTick % 200 !== 0 || - range.endTick % 200 !== 0 || - (index > 0 && range.startTick !== launch.ranges[index - 1]!.endTick) - ) { - throw new Error(`Invalid range ${index} for ${launch.tokenKey}`); - } - amountSum += range.amount; - } - if (amountSum > (TOKEN_SUPPLY * 9_000n) / 10_000n) { - throw new Error(`Range allocation is unsafe for ${launch.tokenKey}`); + if (launch.initialBuyAmount < 0n) { + throw new Error(`Invalid initial buy for ${launch.tokenKey}`); } } + if (launches.filter((launch) => launch.initialBuyAmount > 0n).length !== 2) { + throw new Error("Every scenario must cover two atomic initial buys"); + } const count = (kind: ScenarioAction["kind"]): number => flattened.filter((action) => action.kind === kind).length; @@ -340,6 +309,7 @@ export function validateScenario(plan: ScenarioPlan): void { count("distributeFees") !== 3 || count("withdrawLaunchFees") !== 2 || count("withdrawReserve") !== 3 || + count("transferCreator") !== 1 || count("setProtocolRecipient") !== 1 ) { throw new Error("Scenario is missing required lifecycle coverage"); diff --git a/subgraphs/launchpad/schema.graphql b/subgraphs/launchpad/schema.graphql index c1bcfeef..fc5a6273 100644 --- a/subgraphs/launchpad/schema.graphql +++ b/subgraphs/launchpad/schema.graphql @@ -1,230 +1,204 @@ type Launchpad @entity { - # Identity + # Chain-scoped factory identity id: ID! chainId: BigInt! - address: Bytes! - addressHex: String! + address: String! # Deployment configuration - positionManager: Bytes! - positionManagerHex: String! + positionManager: String! + initialFdvUsd: BigInt! + + # Factory-version launch invariants, initialized atomically on first launch. + launchTokenDecimals: Int + launchTokenTotalSupply: BigInt + launchPoolFee: Int + launchPoolTickSpacing: Int # Current protocol configuration - protocolRecipient: Bytes! - protocolRecipientHex: String! + protocolRecipient: String! launchFee: BigInt! defaultSushiFeeBps: Int! protocolReserveBps: Int! - # Aggregate metrics - tokenCount: Int! - creatorCount: Int! - positionCount: Int! - - # Relationships - tokens: [Token!]! @derivedFrom(field: "launchpad") - pools: [Pool!]! @derivedFrom(field: "launchpad") - creators: [Creator!]! @derivedFrom(field: "launchpad") + launches: [Launch!]! @derivedFrom(field: "launchpad") + quoteTokenPriceFeeds: [QuoteTokenPriceFeed!]! @derivedFrom(field: "launchpad") launchFeeWithdrawals: [LaunchFeeWithdrawal!]! @derivedFrom(field: "launchpad") } -type Creator @entity { - # Identity +# Canonical, complete current projection for one launched token and its one +# launch pool/position. Tokens may have other V3 pools; `pool` is only the +# immutable pool created by this launch. +type Launch @entity { id: ID! chainId: BigInt! - address: Bytes! - addressHex: String! - - # Relationships launchpad: Launchpad! - tokens: [Token!]! @derivedFrom(field: "creator") - - # Aggregate metrics - tokenCount: Int! -} -type Token @entity { - # Identity - id: ID! - chainId: BigInt! - address: Bytes! - addressHex: String! + token: String! + initialCreator: String! + creator: String! + quoteToken: String! + pool: String! + launchTokenIsToken0: Boolean! - # Relationships - launchpad: Launchpad! - creator: Creator! - pool: Pool! - quoteToken: Bytes! - quoteTokenHex: String! - - # ERC-20 metadata + # Token identity emitted for this launch. name: String! symbol: String! + + # Factory-cached ERC-20 invariants initialized from the first launch. decimals: Int! totalSupply: BigInt! - # Current fee configuration - sushiFeeBps: Int! + # Immutable launch pricing. + initialFdvUsd: BigInt! + startTick: Int! + + # Factory-cached V3 pool invariants initialized from the first launch. + poolFee: Int! + poolTickSpacing: Int! + + # The single launch position. + positionManager: String! + positionId: BigInt! + tickLower: Int! + tickUpper: Int! + tokenDesired: BigInt! + tokenUsed: BigInt! + liquidity: BigInt! - # Reserve state + # Current fee and reserve state. + sushiFeeBps: Int! reserveBps: Int! reserveAmount: BigInt! reserveUnlockAt: BigInt! reserveWithdrawn: Boolean! reserveWithdrawal: ReserveWithdrawal + initialBuy: InitialBuy + + feeDistributions: [FeeDistribution!]! @derivedFrom(field: "launch") + creatorTransfers: [CreatorTransfer!]! @derivedFrom(field: "launch") - # Fee distribution metrics - totalAmount0Collected: BigInt! - totalAmount1Collected: BigInt! - totalAmount0ToSushi: BigInt! - totalAmount1ToSushi: BigInt! - totalAmount0ToCreator: BigInt! - totalAmount1ToCreator: BigInt! - feeDistributions: [FeeDistribution!]! @derivedFrom(field: "token") - - # Creation metadata - creationTransactionHash: Bytes! - creationTransactionHashHex: String! + # TokenLaunched metadata, plus the completing PositionCreated log. + creationTransactionHash: String! creationLogIndex: BigInt! + positionCreationLogIndex: BigInt! creationBlockNumber: BigInt! - creationBlockHash: Bytes! - creationBlockHashHex: String! + creationBlockHash: String! createdAt: BigInt! } -type Pool @entity { - # Identity +# Internal two-event accumulator. It is removed as soon as PositionCreated +# publishes the complete Launch, so canonical consumers never observe a launch +# without its required position. +type PendingLaunch @entity { id: ID! chainId: BigInt! - address: Bytes! - addressHex: String! - - # Relationships launchpad: Launchpad! - positions: [LaunchPosition!]! @derivedFrom(field: "pool") - - # Immutable pool configuration - token0: Bytes! - token0Hex: String! - token1: Bytes! - token1Hex: String! - fee: Int! - tickSpacing: Int! - positionManager: Bytes! - positionManagerHex: String! - - # Aggregate metrics - positionCount: Int! - - # Creation metadata - creationTransactionHash: Bytes! - creationTransactionHashHex: String! + token: String! + creator: String! + quoteToken: String! + pool: String! + launchTokenIsToken0: Boolean! + name: String! + symbol: String! + startTick: Int! + sushiFeeBps: Int! + reserveBps: Int! + reserveAmount: BigInt! + reserveUnlockAt: BigInt! + creationTransactionHash: String! + creationLogIndex: BigInt! creationBlockNumber: BigInt! - creationBlockHash: Bytes! - creationBlockHashHex: String! + creationBlockHash: String! createdAt: BigInt! } -type LaunchPosition @entity(immutable: true) { - # Identity +type QuoteTokenPriceFeed @entity { id: ID! chainId: BigInt! - positionManager: Bytes! - positionManagerHex: String! - positionId: BigInt! - index: Int! - - # Relationships - pool: Pool! - - # Position configuration - tickLower: Int! - tickUpper: Int! - liquidity: BigInt! + launchpad: Launchpad! + quoteToken: String! + priceFeed: String! + updatedTransactionHash: String! + updatedLogIndex: BigInt! + updatedBlockNumber: BigInt! + updatedBlockHash: String! + updatedAt: BigInt! +} - # Initial amounts - amount0Desired: BigInt! - amount1Desired: BigInt! - amount0: BigInt! - amount1: BigInt! +type CreatorTransfer @entity(immutable: true) { + id: ID! + chainId: BigInt! + launch: Launch! + previousCreator: String! + newCreator: String! + transactionHash: String! + logIndex: BigInt! + blockNumber: BigInt! + blockHash: String! + timestamp: BigInt! +} - # Creation metadata - creationTransactionHash: Bytes! - creationTransactionHashHex: String! - creationLogIndex: BigInt! - creationBlockNumber: BigInt! - creationBlockHash: Bytes! - creationBlockHashHex: String! - createdAt: BigInt! +type InitialBuy @entity(immutable: true) { + id: ID! + chainId: BigInt! + launch: Launch! + creator: String! + recipient: String! + quoteToken: String! + pool: String! + amountIn: BigInt! + amountOut: BigInt! + transactionHash: String! + logIndex: BigInt! + blockNumber: BigInt! + blockHash: String! + timestamp: BigInt! } type FeeDistribution @entity(immutable: true) { - # Identity id: ID! chainId: BigInt! - - # Relationships - token: Token! - pool: Pool! - - # Participants and fee configuration - caller: Bytes! - sushiRecipient: Bytes! - creatorRecipient: Bytes! + launch: Launch! + pool: String! + caller: String! + sushiRecipient: String! + creatorRecipient: String! sushiFeeBps: Int! - - # Distributed amounts amount0Collected: BigInt! amount1Collected: BigInt! amount0ToSushi: BigInt! amount1ToSushi: BigInt! amount0ToCreator: BigInt! amount1ToCreator: BigInt! - - # Event metadata - transactionHash: Bytes! + transactionHash: String! logIndex: BigInt! blockNumber: BigInt! - blockHash: Bytes! + blockHash: String! timestamp: BigInt! } type ReserveWithdrawal @entity(immutable: true) { - # Identity id: ID! chainId: BigInt! - - # Relationships - token: Token! - - # Withdrawal - recipient: Bytes! + launch: Launch! + recipient: String! amount: BigInt! - - # Event metadata - transactionHash: Bytes! + transactionHash: String! logIndex: BigInt! blockNumber: BigInt! - blockHash: Bytes! + blockHash: String! timestamp: BigInt! } type LaunchFeeWithdrawal @entity(immutable: true) { - # Identity id: ID! chainId: BigInt! - - # Relationships launchpad: Launchpad! - - # Withdrawal - recipient: Bytes! + recipient: String! amount: BigInt! - - # Event metadata - transactionHash: Bytes! + transactionHash: String! logIndex: BigInt! blockNumber: BigInt! - blockHash: Bytes! + blockHash: String! timestamp: BigInt! } diff --git a/subgraphs/launchpad/src/mapping.ts b/subgraphs/launchpad/src/mapping.ts new file mode 100644 index 00000000..c75d0d64 --- /dev/null +++ b/subgraphs/launchpad/src/mapping.ts @@ -0,0 +1,21 @@ +export { + handlePositionCreated, + handleTokenLaunched, +} from "./mappings/launch-lifecycle"; +export { + handleCreatorTransferred, + handleSushiFeeBpsUpdated, +} from "./mappings/launch-state"; +export { + handleFeesDistributed, + handleInitialBuyExecuted, + handleProtocolReserveWithdrawn, +} from "./mappings/launch-activity"; +export { + handleDefaultSushiFeeBpsUpdated, + handleLaunchFeesWithdrawn, + handleLaunchFeeUpdated, + handleProtocolRecipientUpdated, + handleProtocolReserveBpsUpdated, + handleQuoteTokenPriceFeedUpdated, +} from "./mappings/launchpad"; diff --git a/subgraphs/launchpad/src/mappings/launch-activity.ts b/subgraphs/launchpad/src/mappings/launch-activity.ts new file mode 100644 index 00000000..8595bb44 --- /dev/null +++ b/subgraphs/launchpad/src/mappings/launch-activity.ts @@ -0,0 +1,142 @@ +import { + FeesDistributed as FeesDistributedEvent, + InitialBuyExecuted as InitialBuyExecutedEvent, + ProtocolReserveWithdrawn as ProtocolReserveWithdrawnEvent, +} from "../../generated/SushiLaunchpad/SushiLaunchpad"; +import { + FeeDistribution, + InitialBuy, + ReserveWithdrawal, +} from "../../generated/schema"; +import { + canonicalHex, + deploymentContext, + eventId, + getOrCreateLaunchpad, + requireLaunch, +} from "./shared"; + +export function handleInitialBuyExecuted(event: InitialBuyExecutedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const launch = requireLaunch(context, event.params.token); + assert( + launch.launchpad == launchpad.id && + launch.pool == canonicalHex(event.params.pool) && + launch.creator == canonicalHex(event.params.creator) && + launch.quoteToken == canonicalHex(event.params.quoteToken) && + launch.initialBuy == null, + "Initial buy does not reconcile for " + launch.id + ); + + const id = eventId(context.chainId, event); + assert(InitialBuy.load(id) == null, "Duplicate initial buy " + id); + const initialBuy = new InitialBuy(id); + initialBuy.chainId = context.chainId; + initialBuy.launch = launch.id; + initialBuy.creator = canonicalHex(event.params.creator); + initialBuy.recipient = canonicalHex(event.params.recipient); + initialBuy.quoteToken = canonicalHex(event.params.quoteToken); + initialBuy.pool = canonicalHex(event.params.pool); + initialBuy.amountIn = event.params.amountIn; + initialBuy.amountOut = event.params.amountOut; + initialBuy.transactionHash = canonicalHex(event.transaction.hash); + initialBuy.logIndex = event.logIndex; + initialBuy.blockNumber = event.block.number; + initialBuy.blockHash = canonicalHex(event.block.hash); + initialBuy.timestamp = event.block.timestamp; + initialBuy.save(); + + launch.initialBuy = initialBuy.id; + launch.save(); +} + +export function handleFeesDistributed(event: FeesDistributedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const launch = requireLaunch(context, event.params.token); + assert( + launch.launchpad == launchpad.id && + launch.pool == canonicalHex(event.params.pool) && + launch.creator == canonicalHex(event.params.creator) && + launch.sushiFeeBps == event.params.sushiFeeBps && + event.params.quoteCollected.equals( + event.params.quoteToSushi.plus(event.params.quoteToCreator) + ) && + event.params.tokenCollected.equals( + event.params.tokenToSushi.plus(event.params.tokenToCreator) + ), + "Fee distribution does not reconcile for " + launch.id + ); + + const id = eventId(context.chainId, event); + assert(FeeDistribution.load(id) == null, "Duplicate fee distribution " + id); + const tokenIs0 = launch.launchTokenIsToken0; + const distribution = new FeeDistribution(id); + distribution.chainId = context.chainId; + distribution.launch = launch.id; + distribution.pool = canonicalHex(event.params.pool); + distribution.caller = canonicalHex(event.params.caller); + distribution.sushiRecipient = canonicalHex(event.params.protocolRecipient); + distribution.creatorRecipient = canonicalHex(event.params.creator); + distribution.sushiFeeBps = event.params.sushiFeeBps; + distribution.amount0Collected = tokenIs0 + ? event.params.tokenCollected + : event.params.quoteCollected; + distribution.amount1Collected = tokenIs0 + ? event.params.quoteCollected + : event.params.tokenCollected; + distribution.amount0ToSushi = tokenIs0 + ? event.params.tokenToSushi + : event.params.quoteToSushi; + distribution.amount1ToSushi = tokenIs0 + ? event.params.quoteToSushi + : event.params.tokenToSushi; + distribution.amount0ToCreator = tokenIs0 + ? event.params.tokenToCreator + : event.params.quoteToCreator; + distribution.amount1ToCreator = tokenIs0 + ? event.params.quoteToCreator + : event.params.tokenToCreator; + distribution.transactionHash = canonicalHex(event.transaction.hash); + distribution.logIndex = event.logIndex; + distribution.blockNumber = event.block.number; + distribution.blockHash = canonicalHex(event.block.hash); + distribution.timestamp = event.block.timestamp; + distribution.save(); +} + +export function handleProtocolReserveWithdrawn( + event: ProtocolReserveWithdrawnEvent +): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const launch = requireLaunch(context, event.params.token); + assert( + launch.launchpad == launchpad.id && + !launch.reserveWithdrawn && + event.params.amount.equals(launch.reserveAmount), + "Reserve withdrawal does not reconcile for " + launch.id + ); + + const id = eventId(context.chainId, event); + assert( + ReserveWithdrawal.load(id) == null, + "Duplicate reserve withdrawal " + id + ); + const withdrawal = new ReserveWithdrawal(id); + withdrawal.chainId = context.chainId; + withdrawal.launch = launch.id; + withdrawal.recipient = canonicalHex(event.params.recipient); + withdrawal.amount = event.params.amount; + withdrawal.transactionHash = canonicalHex(event.transaction.hash); + withdrawal.logIndex = event.logIndex; + withdrawal.blockNumber = event.block.number; + withdrawal.blockHash = canonicalHex(event.block.hash); + withdrawal.timestamp = event.block.timestamp; + withdrawal.save(); + + launch.reserveWithdrawn = true; + launch.reserveWithdrawal = withdrawal.id; + launch.save(); +} diff --git a/subgraphs/launchpad/src/mappings/launch-invariants.ts b/subgraphs/launchpad/src/mappings/launch-invariants.ts new file mode 100644 index 00000000..77d051f0 --- /dev/null +++ b/subgraphs/launchpad/src/mappings/launch-invariants.ts @@ -0,0 +1,93 @@ +import { Address, BigInt, Value } from "@graphprotocol/graph-ts"; +import { LaunchPool as LaunchPoolContract } from "../../generated/SushiLaunchpad/LaunchPool"; +import { LaunchToken as LaunchTokenContract } from "../../generated/SushiLaunchpad/LaunchToken"; +import { Launchpad } from "../../generated/schema"; + +export class LaunchInvariants { + decimals: i32; + totalSupply: BigInt; + poolFee: i32; + poolTickSpacing: i32; + + constructor( + decimals: i32, + totalSupply: BigInt, + poolFee: i32, + poolTickSpacing: i32 + ) { + this.decimals = decimals; + this.totalSupply = totalSupply; + this.poolFee = poolFee; + this.poolTickSpacing = poolTickSpacing; + } +} + +export function getOrInitializeLaunchInvariants( + launchpad: Launchpad, + tokenAddress: Address, + poolAddress: Address +): LaunchInvariants { + const cachedDecimals = launchpad.get("launchTokenDecimals"); + const cachedTotalSupply = launchpad.get("launchTokenTotalSupply"); + const cachedPoolFee = launchpad.get("launchPoolFee"); + const cachedTickSpacing = launchpad.get("launchPoolTickSpacing"); + const cacheIsUnset = + cachedDecimals == null && + cachedTotalSupply == null && + cachedPoolFee == null && + cachedTickSpacing == null; + const cacheIsSet = + cachedDecimals != null && + cachedTotalSupply != null && + cachedPoolFee != null && + cachedTickSpacing != null; + assert( + cacheIsUnset || cacheIsSet, + "Launchpad invariant cache is partially initialized " + launchpad.id + ); + + if (cacheIsUnset) { + const tokenContract = LaunchTokenContract.bind(tokenAddress); + const poolContract = LaunchPoolContract.bind(poolAddress); + const poolTickSpacing = poolContract.tickSpacing(); + assert( + poolTickSpacing > 0, + "Launchpad invariant cache has invalid tick spacing " + launchpad.id + ); + launchpad.launchTokenDecimals = tokenContract.decimals(); + launchpad.launchTokenTotalSupply = tokenContract.totalSupply(); + launchpad.launchPoolFee = poolContract.fee(); + launchpad.launchPoolTickSpacing = poolTickSpacing; + launchpad.save(); + } + + return requireLaunchInvariants(launchpad); +} + +export function requireLaunchInvariants( + launchpad: Launchpad +): LaunchInvariants { + const cachedDecimals = launchpad.get("launchTokenDecimals"); + const cachedTotalSupply = launchpad.get("launchTokenTotalSupply"); + const cachedPoolFee = launchpad.get("launchPoolFee"); + const cachedTickSpacing = launchpad.get("launchPoolTickSpacing"); + assert( + cachedDecimals != null && + cachedTotalSupply != null && + cachedPoolFee != null && + cachedTickSpacing != null, + "Launchpad invariant cache is unavailable " + launchpad.id + ); + + const invariants = new LaunchInvariants( + changetype(cachedDecimals).toI32(), + changetype(cachedTotalSupply).toBigInt(), + changetype(cachedPoolFee).toI32(), + changetype(cachedTickSpacing).toI32() + ); + assert( + invariants.poolTickSpacing > 0, + "Launchpad invariant cache has invalid tick spacing " + launchpad.id + ); + return invariants; +} diff --git a/subgraphs/launchpad/src/mappings/launch-lifecycle.ts b/subgraphs/launchpad/src/mappings/launch-lifecycle.ts new file mode 100644 index 00000000..6b1f47d2 --- /dev/null +++ b/subgraphs/launchpad/src/mappings/launch-lifecycle.ts @@ -0,0 +1,141 @@ +import { BigInt, store } from "@graphprotocol/graph-ts"; +import { + PositionCreated as PositionCreatedEvent, + TokenLaunched as TokenLaunchedEvent, +} from "../../generated/SushiLaunchpad/SushiLaunchpad"; +import { Launch, PendingLaunch } from "../../generated/schema"; +import { + getOrInitializeLaunchInvariants, + requireLaunchInvariants, +} from "./launch-invariants"; +import { + addressId, + canonicalHex, + deploymentContext, + getOrCreateLaunchpad, +} from "./shared"; + +export function handleTokenLaunched(event: TokenLaunchedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const id = addressId(context.chainId, event.params.token); + assert( + Launch.load(id) == null && PendingLaunch.load(id) == null, + "Duplicate token launch " + id + ); + assert( + !event.params.token.equals(event.params.quoteToken), + "Launch token and quote token must differ " + id + ); + + getOrInitializeLaunchInvariants( + launchpad, + event.params.token, + event.params.pool + ); + + const token = canonicalHex(event.params.token); + const quoteToken = canonicalHex(event.params.quoteToken); + const pending = new PendingLaunch(id); + pending.chainId = context.chainId; + pending.launchpad = launchpad.id; + pending.token = token; + pending.creator = canonicalHex(event.params.creator); + pending.quoteToken = quoteToken; + pending.pool = canonicalHex(event.params.pool); + // V3 token ordering is ascending by address. Fixed-width canonical hex + // strings preserve the same ordering without token0/token1 RPC calls. + pending.launchTokenIsToken0 = token < quoteToken; + pending.name = event.params.name; + pending.symbol = event.params.symbol; + pending.startTick = event.params.startTick; + pending.sushiFeeBps = event.params.initialSushiFeeBps; + pending.reserveBps = event.params.reserveBps; + pending.reserveAmount = event.params.reserveAmount; + pending.reserveUnlockAt = event.params.reserveUnlockAt; + pending.creationTransactionHash = canonicalHex(event.transaction.hash); + pending.creationLogIndex = event.logIndex; + pending.creationBlockNumber = event.block.number; + pending.creationBlockHash = canonicalHex(event.block.hash); + pending.createdAt = event.block.timestamp; + pending.save(); +} + +export function handlePositionCreated(event: PositionCreatedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const id = addressId(context.chainId, event.params.token); + const pending = PendingLaunch.load(id); + assert(pending != null, "Position references no pending launch " + id); + const launchData = changetype(pending); + const invariants = requireLaunchInvariants(launchpad); + assert( + launchData.launchpad == launchpad.id && + launchData.pool == canonicalHex(event.params.pool) && + launchData.creationTransactionHash == + canonicalHex(event.transaction.hash) && + event.logIndex.gt(launchData.creationLogIndex) && + Launch.load(id) == null, + "Position does not complete pending launch " + id + ); + assert( + invariants.totalSupply.ge(launchData.reserveAmount), + "Launch reserve exceeds token supply " + id + ); + const expectedTokenDesired = invariants.totalSupply.minus( + launchData.reserveAmount + ); + const launchBoundaryMatches = launchData.launchTokenIsToken0 + ? event.params.tickLower == launchData.startTick + : event.params.tickUpper == -launchData.startTick; + assert( + launchData.startTick % invariants.poolTickSpacing == 0 && + event.params.tickLower % invariants.poolTickSpacing == 0 && + event.params.tickUpper % invariants.poolTickSpacing == 0 && + event.params.tickLower < event.params.tickUpper && + launchBoundaryMatches && + event.params.tokenDesired.equals(expectedTokenDesired) && + event.params.tokenUsed.le(event.params.tokenDesired) && + event.params.liquidity.gt(BigInt.zero()), + "Position configuration does not reconcile for " + id + ); + + const launch = new Launch(id); + launch.chainId = context.chainId; + launch.launchpad = launchpad.id; + launch.token = launchData.token; + launch.initialCreator = launchData.creator; + launch.creator = launchData.creator; + launch.quoteToken = launchData.quoteToken; + launch.pool = launchData.pool; + launch.launchTokenIsToken0 = launchData.launchTokenIsToken0; + launch.name = launchData.name; + launch.symbol = launchData.symbol; + launch.decimals = invariants.decimals; + launch.totalSupply = invariants.totalSupply; + launch.initialFdvUsd = launchpad.initialFdvUsd; + launch.startTick = launchData.startTick; + launch.poolFee = invariants.poolFee; + launch.poolTickSpacing = invariants.poolTickSpacing; + launch.positionManager = launchpad.positionManager; + launch.positionId = event.params.positionId; + launch.tickLower = event.params.tickLower; + launch.tickUpper = event.params.tickUpper; + launch.tokenDesired = event.params.tokenDesired; + launch.tokenUsed = event.params.tokenUsed; + launch.liquidity = event.params.liquidity; + launch.sushiFeeBps = launchData.sushiFeeBps; + launch.reserveBps = launchData.reserveBps; + launch.reserveAmount = launchData.reserveAmount; + launch.reserveUnlockAt = launchData.reserveUnlockAt; + launch.reserveWithdrawn = false; + launch.creationTransactionHash = launchData.creationTransactionHash; + launch.creationLogIndex = launchData.creationLogIndex; + launch.positionCreationLogIndex = event.logIndex; + launch.creationBlockNumber = launchData.creationBlockNumber; + launch.creationBlockHash = launchData.creationBlockHash; + launch.createdAt = launchData.createdAt; + launch.save(); + + store.remove("PendingLaunch", id); +} diff --git a/subgraphs/launchpad/src/mappings/launch-state.ts b/subgraphs/launchpad/src/mappings/launch-state.ts new file mode 100644 index 00000000..89902b08 --- /dev/null +++ b/subgraphs/launchpad/src/mappings/launch-state.ts @@ -0,0 +1,54 @@ +import { + CreatorTransferred as CreatorTransferredEvent, + SushiFeeBpsUpdated as SushiFeeBpsUpdatedEvent, +} from "../../generated/SushiLaunchpad/SushiLaunchpad"; +import { CreatorTransfer } from "../../generated/schema"; +import { + canonicalHex, + deploymentContext, + eventId, + getOrCreateLaunchpad, + requireLaunch, +} from "./shared"; + +export function handleCreatorTransferred(event: CreatorTransferredEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const launch = requireLaunch(context, event.params.token); + assert( + launch.launchpad == launchpad.id && + launch.creator == canonicalHex(event.params.previousCreator), + "Creator transfer does not reconcile for " + launch.id + ); + + const id = eventId(context.chainId, event); + assert(CreatorTransfer.load(id) == null, "Duplicate creator transfer " + id); + const transfer = new CreatorTransfer(id); + transfer.chainId = context.chainId; + transfer.launch = launch.id; + transfer.previousCreator = canonicalHex(event.params.previousCreator); + transfer.newCreator = canonicalHex(event.params.newCreator); + transfer.transactionHash = canonicalHex(event.transaction.hash); + transfer.logIndex = event.logIndex; + transfer.blockNumber = event.block.number; + transfer.blockHash = canonicalHex(event.block.hash); + transfer.timestamp = event.block.timestamp; + transfer.save(); + + launch.creator = canonicalHex(event.params.newCreator); + launch.save(); +} + +export function handleSushiFeeBpsUpdated(event: SushiFeeBpsUpdatedEvent): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const launch = requireLaunch(context, event.params.token); + assert( + launch.launchpad == launchpad.id && + launch.sushiFeeBps == event.params.previousSushiFeeBps, + "Sushi fee update does not reconcile for " + launch.id + ); + + launch.sushiFeeBps = event.params.newSushiFeeBps; + launch.save(); +} diff --git a/subgraphs/launchpad/src/mappings/launchpad.ts b/subgraphs/launchpad/src/mappings/launchpad.ts index b9e0f8ac..f454ebfe 100644 --- a/subgraphs/launchpad/src/mappings/launchpad.ts +++ b/subgraphs/launchpad/src/mappings/launchpad.ts @@ -1,30 +1,29 @@ +import { Address, store } from "@graphprotocol/graph-ts"; import { DefaultSushiFeeBpsUpdated as DefaultSushiFeeBpsUpdatedEvent, LaunchFeesWithdrawn as LaunchFeesWithdrawnEvent, LaunchFeeUpdated as LaunchFeeUpdatedEvent, ProtocolRecipientUpdated as ProtocolRecipientUpdatedEvent, ProtocolReserveBpsUpdated as ProtocolReserveBpsUpdatedEvent, + QuoteTokenPriceFeedUpdated as QuoteTokenPriceFeedUpdatedEvent, } from "../../generated/SushiLaunchpad/SushiLaunchpad"; -import { LaunchFeeWithdrawal } from "../../generated/schema"; +import { + LaunchFeeWithdrawal, + QuoteTokenPriceFeed, +} from "../../generated/schema"; import { canonicalHex, deploymentContext, eventId, getOrCreateLaunchpad, -} from "./helpers"; - -const BPS_DENOMINATOR = 10_000; -const MAX_PROTOCOL_RESERVE_BPS = 1_000; + quoteTokenPriceFeedId, +} from "./shared"; export function handleDefaultSushiFeeBpsUpdated( event: DefaultSushiFeeBpsUpdatedEvent ): void { const context = deploymentContext(); const launchpad = getOrCreateLaunchpad(context, event.address); - assert( - event.params.newBps <= BPS_DENOMINATOR, - "Invalid default Sushi fee update for " + launchpad.id - ); launchpad.defaultSushiFeeBps = event.params.newBps; launchpad.save(); @@ -35,10 +34,6 @@ export function handleProtocolReserveBpsUpdated( ): void { const context = deploymentContext(); const launchpad = getOrCreateLaunchpad(context, event.address); - assert( - event.params.newBps <= MAX_PROTOCOL_RESERVE_BPS, - "Invalid protocol reserve update for " + launchpad.id - ); launchpad.protocolReserveBps = event.params.newBps; launchpad.save(); @@ -49,8 +44,7 @@ export function handleProtocolRecipientUpdated( ): void { const context = deploymentContext(); const launchpad = getOrCreateLaunchpad(context, event.address); - launchpad.protocolRecipient = event.params.newRecipient; - launchpad.protocolRecipientHex = canonicalHex(event.params.newRecipient); + launchpad.protocolRecipient = canonicalHex(event.params.newRecipient); launchpad.save(); } @@ -75,21 +69,50 @@ export function handleLaunchFeesWithdrawn( const withdrawal = new LaunchFeeWithdrawal(id); withdrawal.chainId = context.chainId; withdrawal.launchpad = launchpad.id; - withdrawal.recipient = event.params.recipient; + withdrawal.recipient = canonicalHex(event.params.recipient); withdrawal.amount = event.params.amount; - withdrawal.transactionHash = event.transaction.hash; + withdrawal.transactionHash = canonicalHex(event.transaction.hash); withdrawal.logIndex = event.logIndex; withdrawal.blockNumber = event.block.number; - withdrawal.blockHash = event.block.hash; + withdrawal.blockHash = canonicalHex(event.block.hash); withdrawal.timestamp = event.block.timestamp; withdrawal.save(); } -export { - handleFeesDistributed, - handleProtocolReserveWithdrawn, - handleSushiFeeBpsUpdated, - handleTokenLaunched, -} from "./token"; - -export { handlePositionCreated } from "./pool"; +export function handleQuoteTokenPriceFeedUpdated( + event: QuoteTokenPriceFeedUpdatedEvent +): void { + const context = deploymentContext(); + const launchpad = getOrCreateLaunchpad(context, event.address); + const id = quoteTokenPriceFeedId(launchpad, event.params.quoteToken); + let configuration = QuoteTokenPriceFeed.load(id); + if (configuration == null) { + assert( + event.params.previousPriceFeed.equals(Address.zero()), + "New quote token price feed has a nonzero predecessor " + id + ); + if (event.params.newPriceFeed.equals(Address.zero())) { + return; + } + configuration = new QuoteTokenPriceFeed(id); + configuration.chainId = context.chainId; + configuration.launchpad = launchpad.id; + configuration.quoteToken = canonicalHex(event.params.quoteToken); + } else { + assert( + configuration.priceFeed == canonicalHex(event.params.previousPriceFeed), + "Quote token price feed update does not reconcile " + id + ); + if (event.params.newPriceFeed.equals(Address.zero())) { + store.remove("QuoteTokenPriceFeed", id); + return; + } + } + configuration.priceFeed = canonicalHex(event.params.newPriceFeed); + configuration.updatedTransactionHash = canonicalHex(event.transaction.hash); + configuration.updatedLogIndex = event.logIndex; + configuration.updatedBlockNumber = event.block.number; + configuration.updatedBlockHash = canonicalHex(event.block.hash); + configuration.updatedAt = event.block.timestamp; + configuration.save(); +} diff --git a/subgraphs/launchpad/src/mappings/pool.ts b/subgraphs/launchpad/src/mappings/pool.ts deleted file mode 100644 index a979f85f..00000000 --- a/subgraphs/launchpad/src/mappings/pool.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { BigInt } from "@graphprotocol/graph-ts"; -import { PositionCreated as PositionCreatedEvent } from "../../generated/SushiLaunchpad/SushiLaunchpad"; -import { SushiV3Pool } from "../../generated/SushiLaunchpad/SushiV3Pool"; -import { LaunchPosition, Pool, Token } from "../../generated/schema"; -import { - addressId, - canonicalHex, - deploymentContext, - getOrCreateLaunchpad, - positionId, -} from "./helpers"; - -export function handlePositionCreated(event: PositionCreatedEvent): void { - const context = deploymentContext(); - const launchpad = getOrCreateLaunchpad(context, event.address); - const idForToken = addressId(context.chainId, event.params.token); - assert( - Token.load(idForToken) == null, - "Position emitted after token launch " + idForToken - ); - - const idForPool = addressId(context.chainId, event.params.pool); - let pool = Pool.load(idForPool); - if (pool == null) { - const contract = SushiV3Pool.bind(event.params.pool); - const token0 = contract.token0(); - const token1 = contract.token1(); - const tokenIs0 = token0.equals(event.params.token); - const tokenIs1 = token1.equals(event.params.token); - assert( - tokenIs0 != tokenIs1, - "Launch position token must be exactly one side of pool " + idForPool - ); - pool = new Pool(idForPool); - pool.chainId = context.chainId; - pool.launchpad = launchpad.id; - pool.address = event.params.pool; - pool.addressHex = canonicalHex(event.params.pool); - pool.token0 = token0; - pool.token0Hex = canonicalHex(token0); - pool.token1 = token1; - pool.token1Hex = canonicalHex(token1); - pool.fee = contract.fee(); - pool.tickSpacing = contract.tickSpacing(); - pool.positionManager = launchpad.positionManager; - pool.positionManagerHex = canonicalHex(launchpad.positionManager); - pool.positionCount = 0; - pool.creationTransactionHash = event.transaction.hash; - pool.creationTransactionHashHex = canonicalHex(event.transaction.hash); - pool.creationBlockNumber = event.block.number; - pool.creationBlockHash = event.block.hash; - pool.creationBlockHashHex = canonicalHex(event.block.hash); - pool.createdAt = event.block.timestamp; - } else { - const tokenIs0 = pool.token0.equals(event.params.token); - const tokenIs1 = pool.token1.equals(event.params.token); - assert( - pool.launchpad == launchpad.id && - pool.address.equals(event.params.pool) && - pool.positionManager.equals(launchpad.positionManager) && - tokenIs0 != tokenIs1, - "Inconsistent ordered position events for pool " + idForPool - ); - } - - const idForPosition = positionId( - context.chainId, - launchpad.positionManager, - event.params.positionId - ); - assert( - LaunchPosition.load(idForPosition) == null, - "Duplicate launch position " + idForPosition - ); - - const zero = BigInt.zero(); - const tokenIs0 = pool.token0.equals(event.params.token); - const position = new LaunchPosition(idForPosition); - position.chainId = context.chainId; - position.pool = pool.id; - position.positionManager = launchpad.positionManager; - position.positionManagerHex = canonicalHex(launchpad.positionManager); - position.positionId = event.params.positionId; - position.index = pool.positionCount; - position.tickLower = event.params.tickLower; - position.tickUpper = event.params.tickUpper; - position.amount0Desired = tokenIs0 ? event.params.tokenDesired : zero; - position.amount1Desired = tokenIs0 ? zero : event.params.tokenDesired; - position.amount0 = tokenIs0 ? event.params.tokenUsed : zero; - position.amount1 = tokenIs0 ? zero : event.params.tokenUsed; - position.liquidity = event.params.liquidity; - position.creationTransactionHash = event.transaction.hash; - position.creationTransactionHashHex = canonicalHex(event.transaction.hash); - position.creationLogIndex = event.logIndex; - position.creationBlockNumber = event.block.number; - position.creationBlockHash = event.block.hash; - position.creationBlockHashHex = canonicalHex(event.block.hash); - position.createdAt = event.block.timestamp; - position.save(); - - pool.positionCount += 1; - pool.save(); - launchpad.positionCount += 1; - launchpad.save(); -} diff --git a/subgraphs/launchpad/src/mappings/helpers.ts b/subgraphs/launchpad/src/mappings/shared.ts similarity index 51% rename from subgraphs/launchpad/src/mappings/helpers.ts rename to subgraphs/launchpad/src/mappings/shared.ts index 406a687c..c8297e25 100644 --- a/subgraphs/launchpad/src/mappings/helpers.ts +++ b/subgraphs/launchpad/src/mappings/shared.ts @@ -6,7 +6,7 @@ import { ethereum, } from "@graphprotocol/graph-ts"; import { SushiLaunchpad as SushiLaunchpadContract } from "../../generated/SushiLaunchpad/SushiLaunchpad"; -import { Launchpad, Pool, Token } from "../../generated/schema"; +import { Launch, Launchpad } from "../../generated/schema"; export class DeploymentContext { chainId: BigInt; @@ -22,41 +22,33 @@ export function deploymentContext(): DeploymentContext { context.get("chainId") != null, "Launchpad data source context is incomplete" ); - return new DeploymentContext(context.getBigInt("chainId")); } export function addressId(chainId: BigInt, address: Bytes): string { - return chainId.toString().concat(":").concat(address.toHexString()); + return chainId.toString().concat(":").concat(canonicalHex(address)); } export function canonicalHex(value: Bytes): string { return value.toHexString().toLowerCase(); } -export function creatorId(launchpad: Launchpad, address: Bytes): string { - return launchpad.id.concat(":").concat(address.toHexString()); -} - -export function positionId( - chainId: BigInt, - positionManager: Bytes, - onchainPositionId: BigInt -): string { - return addressId(chainId, positionManager) - .concat(":") - .concat(onchainPositionId.toString()); -} - export function eventId(chainId: BigInt, event: ethereum.Event): string { return chainId .toString() .concat(":") - .concat(event.transaction.hash.toHexString()) + .concat(canonicalHex(event.transaction.hash)) .concat(":") .concat(event.logIndex.toString()); } +export function quoteTokenPriceFeedId( + launchpad: Launchpad, + quoteToken: Bytes +): string { + return launchpad.id.concat(":").concat(canonicalHex(quoteToken)); +} + export function getOrCreateLaunchpad( context: DeploymentContext, factory: Address @@ -64,43 +56,31 @@ export function getOrCreateLaunchpad( const id = addressId(context.chainId, factory); let launchpad = Launchpad.load(id); if (launchpad == null) { - // Contract calls establish the first block-visible state. Mutable values - // are subsequently owned exclusively by their dedicated update handlers. + // These deployment/current settings are not present in every event. The + // dedicated update handlers exclusively own them after initialization. const contract = SushiLaunchpadContract.bind(factory); launchpad = new Launchpad(id); launchpad.chainId = context.chainId; const positionManager = contract.positionManager(); const protocolRecipient = contract.protocolRecipient(); - launchpad.address = factory; - launchpad.addressHex = canonicalHex(factory); - launchpad.positionManager = positionManager; - launchpad.positionManagerHex = canonicalHex(positionManager); - launchpad.protocolRecipient = protocolRecipient; - launchpad.protocolRecipientHex = canonicalHex(protocolRecipient); + launchpad.address = canonicalHex(factory); + launchpad.positionManager = canonicalHex(positionManager); + launchpad.initialFdvUsd = contract.INITIAL_FDV_USD(); + launchpad.protocolRecipient = canonicalHex(protocolRecipient); launchpad.launchFee = contract.launchFee(); launchpad.defaultSushiFeeBps = contract.defaultSushiFeeBps(); launchpad.protocolReserveBps = contract.protocolReserveBps(); - launchpad.tokenCount = 0; - launchpad.creatorCount = 0; - launchpad.positionCount = 0; launchpad.save(); } return changetype(launchpad); } -export function requireToken( +export function requireLaunch( context: DeploymentContext, - address: Bytes -): Token { - const id = addressId(context.chainId, address); - const token = Token.load(id); - assert(token != null, "Launchpad event references unknown token " + id); - return changetype(token); -} - -export function requirePool(context: DeploymentContext, address: Bytes): Pool { - const id = addressId(context.chainId, address); - const pool = Pool.load(id); - assert(pool != null, "Launchpad event references unknown pool " + id); - return changetype(pool); + token: Bytes +): Launch { + const id = addressId(context.chainId, token); + const launch = Launch.load(id); + assert(launch != null, "Launchpad event references unknown launch " + id); + return changetype(launch); } diff --git a/subgraphs/launchpad/src/mappings/token.ts b/subgraphs/launchpad/src/mappings/token.ts deleted file mode 100644 index b0df50cf..00000000 --- a/subgraphs/launchpad/src/mappings/token.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { BigInt } from "@graphprotocol/graph-ts"; -import { - FeesDistributed as FeesDistributedEvent, - ProtocolReserveWithdrawn as ProtocolReserveWithdrawnEvent, - SushiFeeBpsUpdated as SushiFeeBpsUpdatedEvent, - TokenLaunched as TokenLaunchedEvent, -} from "../../generated/SushiLaunchpad/SushiLaunchpad"; -import { - Creator, - FeeDistribution, - ReserveWithdrawal, - Token, -} from "../../generated/schema"; -import { - addressId, - canonicalHex, - creatorId, - deploymentContext, - eventId, - getOrCreateLaunchpad, - requirePool, - requireToken, -} from "./helpers"; - -const BPS_DENOMINATOR = 10_000; - -export function handleTokenLaunched(event: TokenLaunchedEvent): void { - const context = deploymentContext(); - const launchpad = getOrCreateLaunchpad(context, event.address); - const idForToken = addressId(context.chainId, event.params.token); - assert( - Token.load(idForToken) == null, - "Duplicate token launch " + idForToken - ); - - const pool = requirePool(context, event.params.pool); - assert( - pool.launchpad == launchpad.id && - ((pool.token0.equals(event.params.token) && - pool.token1.equals(event.params.quoteToken)) || - (pool.token1.equals(event.params.token) && - pool.token0.equals(event.params.quoteToken))) && - event.params.positionCount.equals(BigInt.fromI32(pool.positionCount)), - "Token launch does not reconcile with pool " + pool.id - ); - - const idForCreator = creatorId(launchpad, event.params.creator); - let creator = Creator.load(idForCreator); - if (creator == null) { - creator = new Creator(idForCreator); - creator.chainId = context.chainId; - creator.launchpad = launchpad.id; - creator.address = event.params.creator; - creator.addressHex = canonicalHex(event.params.creator); - creator.tokenCount = 0; - launchpad.creatorCount += 1; - } - creator.tokenCount += 1; - creator.save(); - - const zero = BigInt.zero(); - const token = new Token(idForToken); - token.chainId = context.chainId; - token.launchpad = launchpad.id; - token.address = event.params.token; - token.addressHex = canonicalHex(event.params.token); - token.creator = creator.id; - token.pool = pool.id; - token.quoteToken = event.params.quoteToken; - token.quoteTokenHex = canonicalHex(event.params.quoteToken); - token.name = event.params.name; - token.symbol = event.params.symbol; - token.decimals = event.params.decimals; - token.totalSupply = event.params.totalSupply; - token.sushiFeeBps = event.params.initialSushiFeeBps; - token.reserveBps = event.params.reserveBps; - token.reserveAmount = event.params.reserveAmount; - token.reserveUnlockAt = event.params.reserveUnlockAt; - token.reserveWithdrawn = false; - token.totalAmount0Collected = zero; - token.totalAmount1Collected = zero; - token.totalAmount0ToSushi = zero; - token.totalAmount1ToSushi = zero; - token.totalAmount0ToCreator = zero; - token.totalAmount1ToCreator = zero; - token.creationTransactionHash = event.transaction.hash; - token.creationTransactionHashHex = canonicalHex(event.transaction.hash); - token.creationLogIndex = event.logIndex; - token.creationBlockNumber = event.block.number; - token.creationBlockHash = event.block.hash; - token.creationBlockHashHex = canonicalHex(event.block.hash); - token.createdAt = event.block.timestamp; - token.save(); - - launchpad.tokenCount += 1; - launchpad.save(); -} - -export function handleSushiFeeBpsUpdated(event: SushiFeeBpsUpdatedEvent): void { - const context = deploymentContext(); - const launchpad = getOrCreateLaunchpad(context, event.address); - const token = requireToken(context, event.params.token); - assert( - token.launchpad == launchpad.id && - token.sushiFeeBps == event.params.previousSushiFeeBps && - event.params.newSushiFeeBps <= BPS_DENOMINATOR, - "Sushi fee update does not reconcile for " + token.id - ); - - token.sushiFeeBps = event.params.newSushiFeeBps; - token.save(); -} - -export function handleFeesDistributed(event: FeesDistributedEvent): void { - const context = deploymentContext(); - const launchpad = getOrCreateLaunchpad(context, event.address); - const token = requireToken(context, event.params.token); - const pool = requirePool(context, event.params.pool); - const creator = Creator.load(token.creator); - assert(creator != null, "Token references unknown creator " + token.creator); - const tokenCreator = changetype(creator); - assert( - token.launchpad == launchpad.id && - token.pool == pool.id && - tokenCreator.address.equals(event.params.creator) && - token.sushiFeeBps == event.params.sushiFeeBps && - event.params.quoteCollected.equals( - event.params.quoteToSushi.plus(event.params.quoteToCreator) - ) && - event.params.tokenCollected.equals( - event.params.tokenToSushi.plus(event.params.tokenToCreator) - ), - "Fee distribution does not reconcile for " + token.id - ); - - const id = eventId(context.chainId, event); - assert(FeeDistribution.load(id) == null, "Duplicate fee distribution " + id); - const tokenIs0 = pool.token0.equals(event.params.token); - const distribution = new FeeDistribution(id); - distribution.chainId = context.chainId; - distribution.token = token.id; - distribution.pool = pool.id; - distribution.caller = event.params.caller; - distribution.sushiRecipient = event.params.protocolRecipient; - distribution.creatorRecipient = event.params.creator; - distribution.sushiFeeBps = event.params.sushiFeeBps; - distribution.amount0Collected = tokenIs0 - ? event.params.tokenCollected - : event.params.quoteCollected; - distribution.amount1Collected = tokenIs0 - ? event.params.quoteCollected - : event.params.tokenCollected; - distribution.amount0ToSushi = tokenIs0 - ? event.params.tokenToSushi - : event.params.quoteToSushi; - distribution.amount1ToSushi = tokenIs0 - ? event.params.quoteToSushi - : event.params.tokenToSushi; - distribution.amount0ToCreator = tokenIs0 - ? event.params.tokenToCreator - : event.params.quoteToCreator; - distribution.amount1ToCreator = tokenIs0 - ? event.params.quoteToCreator - : event.params.tokenToCreator; - distribution.transactionHash = event.transaction.hash; - distribution.logIndex = event.logIndex; - distribution.blockNumber = event.block.number; - distribution.blockHash = event.block.hash; - distribution.timestamp = event.block.timestamp; - distribution.save(); - - token.totalAmount0Collected = token.totalAmount0Collected.plus( - distribution.amount0Collected - ); - token.totalAmount1Collected = token.totalAmount1Collected.plus( - distribution.amount1Collected - ); - token.totalAmount0ToSushi = token.totalAmount0ToSushi.plus( - distribution.amount0ToSushi - ); - token.totalAmount1ToSushi = token.totalAmount1ToSushi.plus( - distribution.amount1ToSushi - ); - token.totalAmount0ToCreator = token.totalAmount0ToCreator.plus( - distribution.amount0ToCreator - ); - token.totalAmount1ToCreator = token.totalAmount1ToCreator.plus( - distribution.amount1ToCreator - ); - token.save(); -} - -export function handleProtocolReserveWithdrawn( - event: ProtocolReserveWithdrawnEvent -): void { - const context = deploymentContext(); - const launchpad = getOrCreateLaunchpad(context, event.address); - const token = requireToken(context, event.params.token); - assert( - token.launchpad == launchpad.id && - !token.reserveWithdrawn && - event.params.amount.equals(token.reserveAmount), - "Reserve withdrawal does not reconcile for " + token.id - ); - - const id = eventId(context.chainId, event); - assert( - ReserveWithdrawal.load(id) == null, - "Duplicate reserve withdrawal " + id - ); - const withdrawal = new ReserveWithdrawal(id); - withdrawal.chainId = context.chainId; - withdrawal.token = token.id; - withdrawal.recipient = event.params.recipient; - withdrawal.amount = event.params.amount; - withdrawal.transactionHash = event.transaction.hash; - withdrawal.logIndex = event.logIndex; - withdrawal.blockNumber = event.block.number; - withdrawal.blockHash = event.block.hash; - withdrawal.timestamp = event.block.timestamp; - withdrawal.save(); - - token.reserveWithdrawn = true; - token.reserveWithdrawal = withdrawal.id; - token.save(); -} diff --git a/subgraphs/launchpad/template.yaml b/subgraphs/launchpad/template.yaml index 9c903512..ff55ec60 100644 --- a/subgraphs/launchpad/template.yaml +++ b/subgraphs/launchpad/template.yaml @@ -20,26 +20,33 @@ dataSources: kind: ethereum/events apiVersion: 0.0.9 language: wasm/assemblyscript - file: ./src/mappings/launchpad.ts + file: ./src/mapping.ts entities: - Launchpad - - Creator - - Token - - Pool - - LaunchPosition + - Launch + - PendingLaunch + - QuoteTokenPriceFeed + - CreatorTransfer + - InitialBuy - FeeDistribution - ReserveWithdrawal - LaunchFeeWithdrawal abis: - name: SushiLaunchpad file: ./abis/SushiLaunchpad.json - - name: SushiV3Pool + - name: LaunchToken + file: ../v3/abis/ERC20.json + - name: LaunchPool file: ../v3/abis/pool.json eventHandlers: - - event: TokenLaunched(indexed address,indexed address,indexed address,address,string,string,uint8,uint256,uint16,uint256,uint64,uint16,uint256) + - event: TokenLaunched(indexed address,indexed address,indexed address,address,int24,string,string,uint16,uint256,uint64,uint16) handler: handleTokenLaunched - event: PositionCreated(indexed address,indexed address,indexed uint256,int24,int24,uint256,uint256,uint128) handler: handlePositionCreated + - event: InitialBuyExecuted(indexed address,indexed address,indexed address,address,address,uint256,uint256) + handler: handleInitialBuyExecuted + - event: CreatorTransferred(indexed address,indexed address,indexed address) + handler: handleCreatorTransferred - event: SushiFeeBpsUpdated(indexed address,uint16,uint16) handler: handleSushiFeeBpsUpdated - event: DefaultSushiFeeBpsUpdated(uint16,uint16) @@ -56,4 +63,6 @@ dataSources: handler: handleFeesDistributed - event: ProtocolReserveWithdrawn(indexed address,indexed address,uint256) handler: handleProtocolReserveWithdrawn + - event: QuoteTokenPriceFeedUpdated(indexed address,indexed address,indexed address) + handler: handleQuoteTokenPriceFeedUpdated {{/launchpad.deployments}} diff --git a/subgraphs/launchpad/tests/.latest.json b/subgraphs/launchpad/tests/.latest.json index 788b76b8..f2b23ee7 100644 --- a/subgraphs/launchpad/tests/.latest.json +++ b/subgraphs/launchpad/tests/.latest.json @@ -1,4 +1,4 @@ { "version": "0.6.0", - "timestamp": 1784652490157 + "timestamp": 1785278729212 } \ No newline at end of file diff --git a/subgraphs/launchpad/tests/Launchpad.test.ts b/subgraphs/launchpad/tests/Launchpad.test.ts deleted file mode 100644 index 5320e17e..00000000 --- a/subgraphs/launchpad/tests/Launchpad.test.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { BigInt } from "@graphprotocol/graph-ts"; -import { afterEach, assert, beforeEach, clearStore, test } from "matchstick-as"; -import { - handleDefaultSushiFeeBpsUpdated, - handleFeesDistributed, - handleLaunchFeesWithdrawn, - handleLaunchFeeUpdated, - handlePositionCreated, - handleProtocolRecipientUpdated, - handleProtocolReserveBpsUpdated, - handleProtocolReserveWithdrawn, - handleSushiFeeBpsUpdated, - handleTokenLaunched, -} from "../src/mappings/launchpad"; -import { - CALLER, - CHAIN_ID, - CREATOR, - CREATOR_TWO, - FACTORY, - OTHER_POOL, - POOL, - POOL_THREE, - POSITION_MANAGER, - PROTOCOL_RECIPIENT, - PROTOCOL_RECIPIENT_TWO, - QUOTE_TOKEN, - QUOTE_TOKEN_TWO, - TOKEN, - TOKEN_THREE, - TOKEN_TWO, - createDefaultSushiFeeBpsUpdated, - createFeesDistributed, - createLaunchFeesWithdrawn, - createLaunchFeeUpdated, - createPositionCreated, - createProtocolReserveBpsUpdated, - createProtocolRecipientUpdated, - createProtocolReserveWithdrawn, - createSushiFeeBpsUpdated, - createTokenLaunched, - mockDeploymentContext, - mockV3Pool, -} from "./mocks"; - -const LAUNCHPAD_ID = CHAIN_ID.toString() + ":" + FACTORY.toHexString(); -const TOKEN_ID = CHAIN_ID.toString() + ":" + TOKEN.toHexString(); -const POOL_ID = CHAIN_ID.toString() + ":" + POOL.toHexString(); -const CREATOR_ID = LAUNCHPAD_ID + ":" + CREATOR.toHexString(); -const POSITION_PREFIX = - CHAIN_ID.toString() + ":" + POSITION_MANAGER.toHexString() + ":"; - -beforeEach(() => { - mockDeploymentContext(); -}); - -afterEach(() => { - clearStore(); -}); - -test("builds Launchpad, Token, Pool, and canonical initial positions", () => { - handlePositionCreated(createPositionCreated(101, 10)); - handlePositionCreated(createPositionCreated(102, 11)); - - assert.entityCount("Token", 0); - assert.entityCount("Pool", 1); - assert.fieldEquals("Pool", POOL_ID, "positionCount", "2"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "positionCount", "2"); - - handleTokenLaunched(createTokenLaunched(2, 12)); - - assert.entityCount("Launchpad", 1); - assert.entityCount("Creator", 1); - assert.entityCount("Token", 1); - assert.entityCount("Pool", 1); - assert.entityCount("LaunchPosition", 2); - - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "chainId", CHAIN_ID.toString()); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "address", - FACTORY.toHexString() - ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "addressHex", - FACTORY.toHexString().toLowerCase() - ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "positionManager", - POSITION_MANAGER.toHexString() - ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "positionManagerHex", - POSITION_MANAGER.toHexString().toLowerCase() - ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "protocolRecipient", - PROTOCOL_RECIPIENT.toHexString() - ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "protocolRecipientHex", - PROTOCOL_RECIPIENT.toHexString().toLowerCase() - ); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "launchFee", "500000000000000"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "7000"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "300"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "tokenCount", "1"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "creatorCount", "1"); - - assert.fieldEquals("Creator", CREATOR_ID, "address", CREATOR.toHexString()); - assert.fieldEquals( - "Creator", - CREATOR_ID, - "addressHex", - CREATOR.toHexString().toLowerCase() - ); - assert.fieldEquals("Creator", CREATOR_ID, "tokenCount", "1"); - - assert.fieldEquals("Token", TOKEN_ID, "address", TOKEN.toHexString()); - assert.fieldEquals( - "Token", - TOKEN_ID, - "addressHex", - TOKEN.toHexString().toLowerCase() - ); - assert.fieldEquals("Token", TOKEN_ID, "launchpad", LAUNCHPAD_ID); - assert.fieldEquals("Token", TOKEN_ID, "creator", CREATOR_ID); - assert.fieldEquals("Token", TOKEN_ID, "pool", POOL_ID); - assert.fieldEquals( - "Token", - TOKEN_ID, - "quoteToken", - QUOTE_TOKEN.toHexString() - ); - assert.fieldEquals( - "Token", - TOKEN_ID, - "quoteTokenHex", - QUOTE_TOKEN.toHexString().toLowerCase() - ); - assert.fieldEquals("Token", TOKEN_ID, "name", "Sushi Test"); - assert.fieldEquals("Token", TOKEN_ID, "symbol", "SUSHIT"); - assert.fieldEquals("Token", TOKEN_ID, "decimals", "18"); - assert.fieldEquals("Token", TOKEN_ID, "sushiFeeBps", "7000"); - assert.fieldEquals("Token", TOKEN_ID, "reserveBps", "300"); - assert.fieldEquals("Token", TOKEN_ID, "reserveAmount", "30000000000000000"); - assert.fieldEquals("Token", TOKEN_ID, "reserveUnlockAt", "31536001"); - assert.fieldEquals("Token", TOKEN_ID, "reserveWithdrawn", "false"); - - assert.fieldEquals("Pool", POOL_ID, "token0", TOKEN.toHexString()); - assert.fieldEquals( - "Pool", - POOL_ID, - "token0Hex", - TOKEN.toHexString().toLowerCase() - ); - assert.fieldEquals("Pool", POOL_ID, "token1", QUOTE_TOKEN.toHexString()); - assert.fieldEquals( - "Pool", - POOL_ID, - "token1Hex", - QUOTE_TOKEN.toHexString().toLowerCase() - ); - assert.fieldEquals("Pool", POOL_ID, "fee", "10000"); - assert.fieldEquals("Pool", POOL_ID, "tickSpacing", "200"); - - const firstPositionId = POSITION_PREFIX + "101"; - assert.fieldEquals("LaunchPosition", firstPositionId, "pool", POOL_ID); - assert.fieldEquals( - "LaunchPosition", - firstPositionId, - "positionManagerHex", - POSITION_MANAGER.toHexString().toLowerCase() - ); - assert.fieldEquals("LaunchPosition", firstPositionId, "index", "0"); - assert.fieldEquals("LaunchPosition", firstPositionId, "tickLower", "-400"); - assert.fieldEquals("LaunchPosition", firstPositionId, "tickUpper", "-200"); - assert.fieldEquals( - "LaunchPosition", - firstPositionId, - "amount0Desired", - "1000" - ); - assert.fieldEquals("LaunchPosition", firstPositionId, "amount1Desired", "0"); - assert.fieldEquals("LaunchPosition", firstPositionId, "amount0", "999"); - assert.fieldEquals("LaunchPosition", firstPositionId, "amount1", "0"); -}); - -test("counts distinct creators and canonicalizes either token ordering", () => { - handlePositionCreated(createPositionCreated(101, 10)); - handleTokenLaunched(createTokenLaunched(1, 11)); - - mockV3Pool(OTHER_POOL, QUOTE_TOKEN, TOKEN_TWO); - handlePositionCreated(createPositionCreated(102, 20, OTHER_POOL, TOKEN_TWO)); - handleTokenLaunched( - createTokenLaunched(1, 21, TOKEN_TWO, OTHER_POOL, CREATOR) - ); - - mockV3Pool(POOL_THREE, TOKEN_THREE, QUOTE_TOKEN_TWO); - handlePositionCreated( - createPositionCreated(103, 30, POOL_THREE, TOKEN_THREE) - ); - handleTokenLaunched( - createTokenLaunched( - 1, - 31, - TOKEN_THREE, - POOL_THREE, - CREATOR_TWO, - QUOTE_TOKEN_TWO - ) - ); - - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "tokenCount", "3"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "creatorCount", "2"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "positionCount", "3"); - assert.fieldEquals("Creator", CREATOR_ID, "tokenCount", "2"); - - const secondPoolId = CHAIN_ID.toString() + ":" + OTHER_POOL.toHexString(); - const secondTokenId = CHAIN_ID.toString() + ":" + TOKEN_TWO.toHexString(); - const thirdTokenId = CHAIN_ID.toString() + ":" + TOKEN_THREE.toHexString(); - assert.fieldEquals( - "Token", - secondTokenId, - "quoteToken", - QUOTE_TOKEN.toHexString() - ); - assert.fieldEquals( - "Token", - thirdTokenId, - "quoteToken", - QUOTE_TOKEN_TWO.toHexString() - ); - assert.fieldEquals("Pool", secondPoolId, "token0", QUOTE_TOKEN.toHexString()); - assert.fieldEquals("Pool", secondPoolId, "token1", TOKEN_TWO.toHexString()); - assert.fieldEquals( - "LaunchPosition", - POSITION_PREFIX + "102", - "amount0Desired", - "0" - ); - assert.fieldEquals( - "LaunchPosition", - POSITION_PREFIX + "102", - "amount1Desired", - "1000" - ); - assert.fieldEquals("LaunchPosition", POSITION_PREFIX + "102", "amount0", "0"); - assert.fieldEquals( - "LaunchPosition", - POSITION_PREFIX + "102", - "amount1", - "999" - ); - - handleSushiFeeBpsUpdated( - createSushiFeeBpsUpdated(7_000, 6_500, 40, TOKEN_TWO) - ); - const distribution = createFeesDistributed( - 41, - TOKEN_TWO, - OTHER_POOL, - CREATOR - ); - handleFeesDistributed(distribution); - const distributionId = - CHAIN_ID.toString() + - ":" + - distribution.transaction.hash.toHexString() + - ":41"; - assert.fieldEquals( - "FeeDistribution", - distributionId, - "amount0Collected", - "100" - ); - assert.fieldEquals( - "FeeDistribution", - distributionId, - "amount1Collected", - "200" - ); -}); - -test("tracks current Launchpad defaults", () => { - handleDefaultSushiFeeBpsUpdated( - createDefaultSushiFeeBpsUpdated(7_000, 6_000, 10) - ); - handleProtocolReserveBpsUpdated( - createProtocolReserveBpsUpdated(300, 500, 11) - ); - - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "6000"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "500"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "tokenCount", "0"); -}); - -test("keeps token launch snapshots scoped to Token", () => { - handlePositionCreated(createPositionCreated(101, 10)); - handleTokenLaunched( - createTokenLaunched(1, 11, TOKEN, POOL, CREATOR, QUOTE_TOKEN, 6_000, 500) - ); - - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "7000"); - assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "300"); - assert.fieldEquals("Token", TOKEN_ID, "sushiFeeBps", "6000"); - assert.fieldEquals("Token", TOKEN_ID, "reserveBps", "500"); -}); - -test("tracks Launchpad recipient, launch fee, and fee withdrawals", () => { - const initialFee = BigInt.fromString("500000000000000"); - const updatedFee = BigInt.fromString("1000000000000000"); - const withdrawn = BigInt.fromString("1500000000000000"); - - handleProtocolRecipientUpdated( - createProtocolRecipientUpdated( - PROTOCOL_RECIPIENT, - PROTOCOL_RECIPIENT_TWO, - 10 - ) - ); - handleLaunchFeeUpdated(createLaunchFeeUpdated(initialFee, updatedFee, 11)); - const withdrawal = createLaunchFeesWithdrawn( - PROTOCOL_RECIPIENT_TWO, - withdrawn, - 12 - ); - handleLaunchFeesWithdrawn(withdrawal); - - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "protocolRecipient", - PROTOCOL_RECIPIENT_TWO.toHexString() - ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "protocolRecipientHex", - PROTOCOL_RECIPIENT_TWO.toHexString().toLowerCase() - ); - assert.fieldEquals( - "Launchpad", - LAUNCHPAD_ID, - "launchFee", - updatedFee.toString() - ); - const withdrawalId = - CHAIN_ID.toString() + - ":" + - withdrawal.transaction.hash.toHexString() + - ":12"; - assert.fieldEquals( - "LaunchFeeWithdrawal", - withdrawalId, - "launchpad", - LAUNCHPAD_ID - ); - assert.fieldEquals( - "LaunchFeeWithdrawal", - withdrawalId, - "recipient", - PROTOCOL_RECIPIENT_TWO.toHexString() - ); - assert.fieldEquals( - "LaunchFeeWithdrawal", - withdrawalId, - "amount", - withdrawn.toString() - ); -}); - -test("tracks fee distributions and reserve withdrawals on Token", () => { - handlePositionCreated(createPositionCreated(101, 10)); - handleTokenLaunched(createTokenLaunched(1, 11)); - handleSushiFeeBpsUpdated(createSushiFeeBpsUpdated(7_000, 6_500, 12)); - - const distribution = createFeesDistributed(13); - handleFeesDistributed(distribution); - handleProtocolReserveWithdrawn(createProtocolReserveWithdrawn(14)); - - assert.fieldEquals("Token", TOKEN_ID, "sushiFeeBps", "6500"); - assert.fieldEquals("Token", TOKEN_ID, "totalAmount0Collected", "200"); - assert.fieldEquals("Token", TOKEN_ID, "totalAmount1Collected", "100"); - assert.fieldEquals("Token", TOKEN_ID, "totalAmount0ToSushi", "130"); - assert.fieldEquals("Token", TOKEN_ID, "totalAmount1ToSushi", "65"); - assert.fieldEquals("Token", TOKEN_ID, "totalAmount0ToCreator", "70"); - assert.fieldEquals("Token", TOKEN_ID, "totalAmount1ToCreator", "35"); - assert.fieldEquals("Token", TOKEN_ID, "reserveWithdrawn", "true"); - - const distributionId = - CHAIN_ID.toString() + - ":" + - distribution.transaction.hash.toHexString() + - ":13"; - assert.fieldEquals("FeeDistribution", distributionId, "token", TOKEN_ID); - assert.fieldEquals("FeeDistribution", distributionId, "pool", POOL_ID); - assert.fieldEquals( - "FeeDistribution", - distributionId, - "caller", - CALLER.toHexString() - ); - assert.fieldEquals( - "FeeDistribution", - distributionId, - "sushiRecipient", - PROTOCOL_RECIPIENT.toHexString() - ); - assert.fieldEquals( - "FeeDistribution", - distributionId, - "creatorRecipient", - CREATOR.toHexString() - ); - assert.fieldEquals( - "FeeDistribution", - distributionId, - "amount0Collected", - "200" - ); - assert.fieldEquals( - "FeeDistribution", - distributionId, - "amount1Collected", - "100" - ); - - const withdrawalId = - CHAIN_ID.toString() + - ":" + - distribution.transaction.hash.toHexString() + - ":14"; - assert.fieldEquals("Token", TOKEN_ID, "reserveWithdrawal", withdrawalId); - assert.fieldEquals("ReserveWithdrawal", withdrawalId, "token", TOKEN_ID); - assert.fieldEquals( - "ReserveWithdrawal", - withdrawalId, - "recipient", - PROTOCOL_RECIPIENT.toHexString() - ); - assert.fieldEquals( - "ReserveWithdrawal", - withdrawalId, - "amount", - "30000000000000000" - ); -}); - -test( - "fails deterministically when ordered position identities disagree", - () => { - handlePositionCreated(createPositionCreated(101, 10)); - handlePositionCreated(createPositionCreated(102, 11, POOL, TOKEN_TWO)); - }, - true -); - -test( - "fails deterministically when the launch count does not reconcile", - () => { - handlePositionCreated(createPositionCreated(101, 10)); - handleTokenLaunched(createTokenLaunched(2, 11)); - }, - true -); - -test( - "fails deterministically when the emitted quote token does not match the pool", - () => { - handlePositionCreated(createPositionCreated(101, 10)); - handleTokenLaunched( - createTokenLaunched(1, 11, TOKEN, POOL, CREATOR, QUOTE_TOKEN_TWO) - ); - }, - true -); diff --git a/subgraphs/launchpad/tests/launch-activity.test.ts b/subgraphs/launchpad/tests/launch-activity.test.ts new file mode 100644 index 00000000..e59c260f --- /dev/null +++ b/subgraphs/launchpad/tests/launch-activity.test.ts @@ -0,0 +1,84 @@ +import { afterEach, assert, beforeEach, clearStore, test } from "matchstick-as"; +import { + handleCreatorTransferred, + handleFeesDistributed, + handleInitialBuyExecuted, + handlePositionCreated, + handleProtocolReserveWithdrawn, + handleSushiFeeBpsUpdated, + handleTokenLaunched, +} from "../src/mapping"; +import { + CALLER, + CHAIN_ID, + CREATOR_TWO, + POOL, + TOKEN, + createCreatorTransferred, + createFeesDistributed, + createInitialBuyExecuted, + createPositionCreated, + createProtocolReserveWithdrawn, + createSushiFeeBpsUpdated, + createTokenLaunched, + mockDeploymentContext, +} from "./mocks"; + +const LAUNCH_ID = CHAIN_ID.toString() + ":" + TOKEN.toHexString(); + +beforeEach(() => { + mockDeploymentContext(); +}); + +afterEach(() => { + clearStore(); +}); + +test("records immutable per-launch activity", () => { + handleTokenLaunched(createTokenLaunched(10)); + handlePositionCreated(createPositionCreated(101, 11)); + handleInitialBuyExecuted(createInitialBuyExecuted(12)); + handleSushiFeeBpsUpdated(createSushiFeeBpsUpdated(7_000, 6_500, 13)); + handleCreatorTransferred(createCreatorTransferred(14)); + + const distributionEvent = createFeesDistributed(15, TOKEN, POOL, CREATOR_TWO); + handleFeesDistributed(distributionEvent); + const reserveEvent = createProtocolReserveWithdrawn(16); + handleProtocolReserveWithdrawn(reserveEvent); + + assert.fieldEquals("Launch", LAUNCH_ID, "reserveWithdrawn", "true"); + assert.entityCount("InitialBuy", 1); + assert.entityCount("FeeDistribution", 1); + assert.entityCount("ReserveWithdrawal", 1); + + const distributionId = + CHAIN_ID.toString() + + ":" + + distributionEvent.transaction.hash.toHexString() + + ":15"; + assert.fieldEquals("FeeDistribution", distributionId, "launch", LAUNCH_ID); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "pool", + POOL.toHexString().toLowerCase() + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "caller", + CALLER.toHexString().toLowerCase() + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "amount0Collected", + "200" + ); + assert.fieldEquals( + "FeeDistribution", + distributionId, + "amount1Collected", + "100" + ); +}); diff --git a/subgraphs/launchpad/tests/launch-lifecycle.test.ts b/subgraphs/launchpad/tests/launch-lifecycle.test.ts new file mode 100644 index 00000000..6798b0b8 --- /dev/null +++ b/subgraphs/launchpad/tests/launch-lifecycle.test.ts @@ -0,0 +1,276 @@ +import { afterEach, assert, beforeEach, clearStore, test } from "matchstick-as"; +import { Launchpad } from "../generated/schema"; +import { + handleDefaultSushiFeeBpsUpdated, + handlePositionCreated, + handleTokenLaunched, +} from "../src/mapping"; +import { + CHAIN_ID, + CREATOR, + FACTORY, + INITIAL_FDV_USD, + LAUNCH_POOL_FEE, + LAUNCH_POOL_TICK_SPACING, + LAUNCH_TOKEN_DECIMALS, + LAUNCH_TOKEN_DESIRED, + LAUNCH_TOKEN_TOTAL_SUPPLY, + LAUNCH_TOKEN_USED, + OTHER_POOL, + POOL, + POSITION_MANAGER, + QUOTE_TOKEN, + TOKEN, + TOKEN_TWO, + createDefaultSushiFeeBpsUpdated, + createPositionCreated, + createPositionCreatedWithInvalidTicks, + createTokenLaunched, + mockDeploymentContext, +} from "./mocks"; + +const LAUNCHPAD_ID = CHAIN_ID.toString() + ":" + FACTORY.toHexString(); +const LAUNCH_ID = CHAIN_ID.toString() + ":" + TOKEN.toHexString(); + +beforeEach(() => { + mockDeploymentContext(); +}); + +afterEach(() => { + clearStore(); +}); + +test("publishes Launch only after the required position completes it", () => { + const tokenEvent = createTokenLaunched(10); + handleTokenLaunched(tokenEvent); + + assert.entityCount("Launchpad", 1); + assert.entityCount("PendingLaunch", 1); + assert.entityCount("Launch", 0); + assert.fieldEquals( + "PendingLaunch", + LAUNCH_ID, + "pool", + POOL.toHexString().toLowerCase() + ); + assert.fieldEquals("PendingLaunch", LAUNCH_ID, "startTick", "-12400"); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "launchTokenDecimals", + LAUNCH_TOKEN_DECIMALS.toString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "launchTokenTotalSupply", + LAUNCH_TOKEN_TOTAL_SUPPLY.toString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "launchPoolFee", + LAUNCH_POOL_FEE.toString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "launchPoolTickSpacing", + LAUNCH_POOL_TICK_SPACING.toString() + ); + + handlePositionCreated(createPositionCreated(101, 11)); + + assert.entityCount("PendingLaunch", 0); + assert.entityCount("Launch", 1); + assert.fieldEquals("Launch", LAUNCH_ID, "launchpad", LAUNCHPAD_ID); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "token", + TOKEN.toHexString().toLowerCase() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "initialCreator", + CREATOR.toHexString().toLowerCase() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "creator", + CREATOR.toHexString().toLowerCase() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "quoteToken", + QUOTE_TOKEN.toHexString().toLowerCase() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "pool", + POOL.toHexString().toLowerCase() + ); + assert.fieldEquals("Launch", LAUNCH_ID, "launchTokenIsToken0", "true"); + assert.fieldEquals("Launch", LAUNCH_ID, "name", "Sushi Test"); + assert.fieldEquals("Launch", LAUNCH_ID, "symbol", "SUSHIT"); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "decimals", + LAUNCH_TOKEN_DECIMALS.toString() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "totalSupply", + LAUNCH_TOKEN_TOTAL_SUPPLY.toString() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "initialFdvUsd", + INITIAL_FDV_USD.toString() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "initialFdvUsd", + INITIAL_FDV_USD.toString() + ); + assert.fieldEquals("Launch", LAUNCH_ID, "startTick", "-12400"); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "poolFee", + LAUNCH_POOL_FEE.toString() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "poolTickSpacing", + LAUNCH_POOL_TICK_SPACING.toString() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "positionManager", + POSITION_MANAGER.toHexString().toLowerCase() + ); + assert.fieldEquals("Launch", LAUNCH_ID, "positionId", "101"); + assert.fieldEquals("Launch", LAUNCH_ID, "tickLower", "-12400"); + assert.fieldEquals("Launch", LAUNCH_ID, "tickUpper", "887200"); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "tokenDesired", + LAUNCH_TOKEN_DESIRED.toString() + ); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "tokenUsed", + LAUNCH_TOKEN_USED.toString() + ); + assert.fieldEquals("Launch", LAUNCH_ID, "liquidity", "500"); + assert.fieldEquals("Launch", LAUNCH_ID, "sushiFeeBps", "7000"); + assert.fieldEquals("Launch", LAUNCH_ID, "reserveBps", "300"); + assert.fieldEquals("Launch", LAUNCH_ID, "reserveWithdrawn", "false"); + assert.fieldEquals("Launch", LAUNCH_ID, "creationLogIndex", "10"); + assert.fieldEquals("Launch", LAUNCH_ID, "positionCreationLogIndex", "11"); + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "creationTransactionHash", + tokenEvent.transaction.hash.toHexString().toLowerCase() + ); +}); + +test("derives canonical pool ordering without token0/token1 RPC calls", () => { + const secondLaunchId = CHAIN_ID.toString() + ":" + TOKEN_TWO.toHexString(); + handleTokenLaunched(createTokenLaunched(10)); + handlePositionCreated(createPositionCreated(101, 11)); + + // TOKEN_TWO and OTHER_POOL deliberately have no getter mocks. The second + // launch must reuse the factory-version cache initialized above. + handleTokenLaunched( + createTokenLaunched(20, TOKEN_TWO, OTHER_POOL, CREATOR, QUOTE_TOKEN) + ); + handlePositionCreated(createPositionCreated(102, 21, OTHER_POOL, TOKEN_TWO)); + + assert.fieldEquals( + "Launch", + secondLaunchId, + "pool", + OTHER_POOL.toHexString().toLowerCase() + ); + assert.fieldEquals("Launch", secondLaunchId, "launchTokenIsToken0", "false"); + assert.fieldEquals( + "Launch", + secondLaunchId, + "decimals", + LAUNCH_TOKEN_DECIMALS.toString() + ); + assert.fieldEquals( + "Launch", + secondLaunchId, + "totalSupply", + LAUNCH_TOKEN_TOTAL_SUPPLY.toString() + ); + assert.fieldEquals( + "Launch", + secondLaunchId, + "poolFee", + LAUNCH_POOL_FEE.toString() + ); + assert.fieldEquals( + "Launch", + secondLaunchId, + "poolTickSpacing", + LAUNCH_POOL_TICK_SPACING.toString() + ); +}); + +test( + "fails deterministically when PositionCreated has no pending launch", + () => { + handlePositionCreated(createPositionCreated(101, 10)); + }, + true +); + +test( + "fails deterministically when PositionCreated names another pool", + () => { + handleTokenLaunched(createTokenLaunched(10)); + handlePositionCreated(createPositionCreated(101, 11, OTHER_POOL)); + }, + true +); + +test( + "fails deterministically when the completed position has invalid ticks", + () => { + handleTokenLaunched(createTokenLaunched(10)); + handlePositionCreated(createPositionCreatedWithInvalidTicks(101, 11)); + }, + true +); + +test( + "fails deterministically when the Launchpad cache is partially initialized", + () => { + handleDefaultSushiFeeBpsUpdated( + createDefaultSushiFeeBpsUpdated(7_000, 6_000, 9) + ); + const launchpad = changetype(Launchpad.load(LAUNCHPAD_ID)); + launchpad.launchTokenDecimals = LAUNCH_TOKEN_DECIMALS; + launchpad.save(); + + handleTokenLaunched(createTokenLaunched(10)); + }, + true +); diff --git a/subgraphs/launchpad/tests/launch-state.test.ts b/subgraphs/launchpad/tests/launch-state.test.ts new file mode 100644 index 00000000..24629c7a --- /dev/null +++ b/subgraphs/launchpad/tests/launch-state.test.ts @@ -0,0 +1,64 @@ +import { afterEach, assert, beforeEach, clearStore, test } from "matchstick-as"; +import { + handleCreatorTransferred, + handlePositionCreated, + handleSushiFeeBpsUpdated, + handleTokenLaunched, +} from "../src/mapping"; +import { + CHAIN_ID, + CREATOR, + CREATOR_TWO, + TOKEN, + createCreatorTransferred, + createPositionCreated, + createSushiFeeBpsUpdated, + createTokenLaunched, + mockDeploymentContext, +} from "./mocks"; + +const LAUNCH_ID = CHAIN_ID.toString() + ":" + TOKEN.toHexString(); + +beforeEach(() => { + mockDeploymentContext(); +}); + +afterEach(() => { + clearStore(); +}); + +test("tracks mutable launch state and creator transfer history", () => { + handleTokenLaunched(createTokenLaunched(10)); + handlePositionCreated(createPositionCreated(101, 11)); + handleSushiFeeBpsUpdated(createSushiFeeBpsUpdated(7_000, 6_500, 13)); + const transferEvent = createCreatorTransferred(14); + handleCreatorTransferred(transferEvent); + + assert.fieldEquals( + "Launch", + LAUNCH_ID, + "creator", + CREATOR_TWO.toHexString().toLowerCase() + ); + assert.fieldEquals("Launch", LAUNCH_ID, "sushiFeeBps", "6500"); + assert.entityCount("CreatorTransfer", 1); + + const transferId = + CHAIN_ID.toString() + + ":" + + transferEvent.transaction.hash.toHexString() + + ":14"; + assert.fieldEquals("CreatorTransfer", transferId, "launch", LAUNCH_ID); + assert.fieldEquals( + "CreatorTransfer", + transferId, + "previousCreator", + CREATOR.toHexString().toLowerCase() + ); + assert.fieldEquals( + "CreatorTransfer", + transferId, + "newCreator", + CREATOR_TWO.toHexString().toLowerCase() + ); +}); diff --git a/subgraphs/launchpad/tests/launchpad.test.ts b/subgraphs/launchpad/tests/launchpad.test.ts new file mode 100644 index 00000000..834a951b --- /dev/null +++ b/subgraphs/launchpad/tests/launchpad.test.ts @@ -0,0 +1,98 @@ +import { Address, BigInt } from "@graphprotocol/graph-ts"; +import { afterEach, assert, beforeEach, clearStore, test } from "matchstick-as"; +import { + handleDefaultSushiFeeBpsUpdated, + handleLaunchFeesWithdrawn, + handleLaunchFeeUpdated, + handleProtocolRecipientUpdated, + handleProtocolReserveBpsUpdated, + handleQuoteTokenPriceFeedUpdated, +} from "../src/mapping"; +import { + CHAIN_ID, + FACTORY, + PRICE_FEED, + PRICE_FEED_TWO, + PROTOCOL_RECIPIENT, + PROTOCOL_RECIPIENT_TWO, + QUOTE_TOKEN, + createDefaultSushiFeeBpsUpdated, + createLaunchFeesWithdrawn, + createLaunchFeeUpdated, + createProtocolRecipientUpdated, + createProtocolReserveBpsUpdated, + createQuoteTokenPriceFeedUpdated, + mockDeploymentContext, +} from "./mocks"; + +const LAUNCHPAD_ID = CHAIN_ID.toString() + ":" + FACTORY.toHexString(); + +beforeEach(() => { + mockDeploymentContext(); +}); + +afterEach(() => { + clearStore(); +}); + +test("tracks current factory configuration and quote-token feeds", () => { + handleDefaultSushiFeeBpsUpdated( + createDefaultSushiFeeBpsUpdated(7_000, 6_000, 10) + ); + handleProtocolReserveBpsUpdated( + createProtocolReserveBpsUpdated(300, 500, 11) + ); + handleProtocolRecipientUpdated( + createProtocolRecipientUpdated( + PROTOCOL_RECIPIENT, + PROTOCOL_RECIPIENT_TWO, + 12 + ) + ); + handleLaunchFeeUpdated( + createLaunchFeeUpdated( + BigInt.fromString("500000000000000"), + BigInt.fromString("1000000000000000"), + 13 + ) + ); + handleQuoteTokenPriceFeedUpdated( + createQuoteTokenPriceFeedUpdated(Address.zero(), PRICE_FEED, 14) + ); + handleQuoteTokenPriceFeedUpdated( + createQuoteTokenPriceFeedUpdated(PRICE_FEED, PRICE_FEED_TWO, 15) + ); + const priceFeedId = LAUNCHPAD_ID + ":" + QUOTE_TOKEN.toHexString(); + assert.fieldEquals( + "QuoteTokenPriceFeed", + priceFeedId, + "priceFeed", + PRICE_FEED_TWO.toHexString().toLowerCase() + ); + handleQuoteTokenPriceFeedUpdated( + createQuoteTokenPriceFeedUpdated(PRICE_FEED_TWO, Address.zero(), 16) + ); + assert.entityCount("QuoteTokenPriceFeed", 0); + const withdrawal = createLaunchFeesWithdrawn( + PROTOCOL_RECIPIENT_TWO, + BigInt.fromI32(123), + 17 + ); + handleLaunchFeesWithdrawn(withdrawal); + + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "defaultSushiFeeBps", "6000"); + assert.fieldEquals("Launchpad", LAUNCHPAD_ID, "protocolReserveBps", "500"); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "protocolRecipient", + PROTOCOL_RECIPIENT_TWO.toHexString().toLowerCase() + ); + assert.fieldEquals( + "Launchpad", + LAUNCHPAD_ID, + "launchFee", + "1000000000000000" + ); + assert.entityCount("LaunchFeeWithdrawal", 1); +}); diff --git a/subgraphs/launchpad/tests/mocks.ts b/subgraphs/launchpad/tests/mocks.ts index 60346d2f..5d5ed523 100644 --- a/subgraphs/launchpad/tests/mocks.ts +++ b/subgraphs/launchpad/tests/mocks.ts @@ -10,19 +10,35 @@ import { newMockEvent, } from "matchstick-as"; import { + CreatorTransferred, DefaultSushiFeeBpsUpdated, FeesDistributed, + InitialBuyExecuted, LaunchFeesWithdrawn, LaunchFeeUpdated, PositionCreated, ProtocolRecipientUpdated, ProtocolReserveBpsUpdated, ProtocolReserveWithdrawn, + QuoteTokenPriceFeedUpdated, SushiFeeBpsUpdated, TokenLaunched, } from "../generated/SushiLaunchpad/SushiLaunchpad"; export const CHAIN_ID = BigInt.fromI32(4663); +export const INITIAL_FDV_USD = BigInt.fromI32(12_345); +export const LAUNCH_TOKEN_DECIMALS = 6; +export const LAUNCH_TOKEN_TOTAL_SUPPLY = BigInt.fromString( + "2000000000000000000000000000" +); +export const LAUNCH_POOL_FEE = 3_000; +export const LAUNCH_POOL_TICK_SPACING = 100; +export const LAUNCH_TOKEN_DESIRED = BigInt.fromString( + "1970000000000000000000000000" +); +export const LAUNCH_TOKEN_USED = BigInt.fromString( + "1969999999999999999999999999" +); export const FACTORY = Address.fromString( "0x1000000000000000000000000000000000000001" ); @@ -68,6 +84,27 @@ export const QUOTE_TOKEN_TWO = Address.fromString( export const POSITION_MANAGER = Address.fromString( "0x9000000000000000000000000000000000000009" ); +export const PRICE_FEED = Address.fromString( + "0x1100000000000000000000000000000000000011" +); +export const PRICE_FEED_TWO = Address.fromString( + "0x1200000000000000000000000000000000000012" +); + +function mockLaunchContracts(token: Address, pool: Address): void { + createMockedFunction(token, "decimals", "decimals():(uint8)").returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(LAUNCH_TOKEN_DECIMALS)), + ]); + createMockedFunction(token, "totalSupply", "totalSupply():(uint256)").returns( + [ethereum.Value.fromUnsignedBigInt(LAUNCH_TOKEN_TOTAL_SUPPLY)] + ); + createMockedFunction(pool, "fee", "fee():(uint24)").returns([ + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(LAUNCH_POOL_FEE)), + ]); + createMockedFunction(pool, "tickSpacing", "tickSpacing():(int24)").returns([ + ethereum.Value.fromI32(LAUNCH_POOL_TICK_SPACING), + ]); +} export function mockDeploymentContext(): void { const context = new DataSourceContext(); @@ -79,6 +116,11 @@ export function mockDeploymentContext(): void { "positionManager", "positionManager():(address)" ).returns([ethereum.Value.fromAddress(POSITION_MANAGER)]); + createMockedFunction( + FACTORY, + "INITIAL_FDV_USD", + "INITIAL_FDV_USD():(uint256)" + ).returns([ethereum.Value.fromUnsignedBigInt(INITIAL_FDV_USD)]); createMockedFunction( FACTORY, "defaultSushiFeeBps", @@ -97,26 +139,8 @@ export function mockDeploymentContext(): void { createMockedFunction(FACTORY, "launchFee", "launchFee():(uint256)").returns([ ethereum.Value.fromUnsignedBigInt(BigInt.fromString("500000000000000")), ]); - mockV3Pool(POOL, TOKEN, QUOTE_TOKEN); -} -export function mockV3Pool( - pool: Address, - token0: Address, - token1: Address -): void { - createMockedFunction(pool, "token0", "token0():(address)").returns([ - ethereum.Value.fromAddress(token0), - ]); - createMockedFunction(pool, "token1", "token1():(address)").returns([ - ethereum.Value.fromAddress(token1), - ]); - createMockedFunction(pool, "fee", "fee():(uint24)").returns([ - ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(10_000)), - ]); - createMockedFunction(pool, "tickSpacing", "tickSpacing():(int24)").returns([ - ethereum.Value.fromI32(200), - ]); + mockLaunchContracts(TOKEN, POOL); } function configureEvent(event: ethereum.Event, logIndex: i32): void { @@ -132,6 +156,8 @@ export function createPositionCreated( ): PositionCreated { const event = changetype(newMockEvent()); configureEvent(event, logIndex); + const tokenIs0 = + token.toHexString().toLowerCase() < QUOTE_TOKEN.toHexString().toLowerCase(); event.parameters = new Array(); event.parameters.push( new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) @@ -146,21 +172,27 @@ export function createPositionCreated( ) ); event.parameters.push( - new ethereum.EventParam("tickLower", ethereum.Value.fromI32(-400)) + new ethereum.EventParam( + "tickLower", + ethereum.Value.fromI32(tokenIs0 ? -12_400 : -887_200) + ) ); event.parameters.push( - new ethereum.EventParam("tickUpper", ethereum.Value.fromI32(-200)) + new ethereum.EventParam( + "tickUpper", + ethereum.Value.fromI32(tokenIs0 ? 887_200 : 12_400) + ) ); event.parameters.push( new ethereum.EventParam( "tokenDesired", - ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(1_000)) + ethereum.Value.fromUnsignedBigInt(LAUNCH_TOKEN_DESIRED) ) ); event.parameters.push( new ethereum.EventParam( "tokenUsed", - ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(999)) + ethereum.Value.fromUnsignedBigInt(LAUNCH_TOKEN_USED) ) ); event.parameters.push( @@ -172,8 +204,19 @@ export function createPositionCreated( return event; } +export function createPositionCreatedWithInvalidTicks( + onchainPositionId: i32, + logIndex: i32 +): PositionCreated { + const event = createPositionCreated(onchainPositionId, logIndex); + event.parameters[3] = new ethereum.EventParam( + "tickLower", + ethereum.Value.fromI32(-400) + ); + return event; +} + export function createTokenLaunched( - positionCount: i32, logIndex: i32, token: Address = TOKEN, pool: Address = POOL, @@ -200,6 +243,9 @@ export function createTokenLaunched( ethereum.Value.fromAddress(quoteToken) ) ); + event.parameters.push( + new ethereum.EventParam("startTick", ethereum.Value.fromI32(-12_400)) + ); event.parameters.push( new ethereum.EventParam("name", ethereum.Value.fromString("Sushi Test")) ); @@ -207,41 +253,97 @@ export function createTokenLaunched( new ethereum.EventParam("symbol", ethereum.Value.fromString("SUSHIT")) ); event.parameters.push( - new ethereum.EventParam("decimals", ethereum.Value.fromI32(18)) + new ethereum.EventParam("reserveBps", ethereum.Value.fromI32(reserveBps)) ); event.parameters.push( new ethereum.EventParam( - "totalSupply", + "reserveAmount", ethereum.Value.fromUnsignedBigInt( - BigInt.fromString("1000000000000000000") + BigInt.fromString("30000000000000000000000000") ) ) ); event.parameters.push( - new ethereum.EventParam("reserveBps", ethereum.Value.fromI32(reserveBps)) + new ethereum.EventParam( + "reserveUnlockAt", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(31_536_001)) + ) ); event.parameters.push( new ethereum.EventParam( - "reserveAmount", - ethereum.Value.fromUnsignedBigInt(BigInt.fromString("30000000000000000")) + "initialSushiFeeBps", + ethereum.Value.fromI32(initialSushiFeeBps) + ) + ); + return event; +} + +export function createInitialBuyExecuted( + logIndex: i32, + token: Address = TOKEN, + pool: Address = POOL, + creator: Address = CREATOR, + quoteToken: Address = QUOTE_TOKEN, + recipient: Address = CREATOR +): InitialBuyExecuted { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("creator", ethereum.Value.fromAddress(creator)) + ); + event.parameters.push( + new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) + ); + event.parameters.push( + new ethereum.EventParam("pool", ethereum.Value.fromAddress(pool)) + ); + event.parameters.push( + new ethereum.EventParam("recipient", ethereum.Value.fromAddress(recipient)) + ); + event.parameters.push( + new ethereum.EventParam( + "quoteToken", + ethereum.Value.fromAddress(quoteToken) ) ); event.parameters.push( new ethereum.EventParam( - "reserveUnlockAt", - ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(31_536_001)) + "amountIn", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(100)) ) ); event.parameters.push( new ethereum.EventParam( - "initialSushiFeeBps", - ethereum.Value.fromI32(initialSushiFeeBps) + "amountOut", + ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(99)) ) ); + return event; +} + +export function createCreatorTransferred( + logIndex: i32, + token: Address = TOKEN, + previousCreator: Address = CREATOR, + newCreator: Address = CREATOR_TWO +): CreatorTransferred { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam("token", ethereum.Value.fromAddress(token)) + ); event.parameters.push( new ethereum.EventParam( - "positionCount", - ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(positionCount)) + "previousCreator", + ethereum.Value.fromAddress(previousCreator) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "newCreator", + ethereum.Value.fromAddress(newCreator) ) ); return event; @@ -453,7 +555,39 @@ export function createProtocolReserveWithdrawn( event.parameters.push( new ethereum.EventParam( "amount", - ethereum.Value.fromUnsignedBigInt(BigInt.fromString("30000000000000000")) + ethereum.Value.fromUnsignedBigInt( + BigInt.fromString("30000000000000000000000000") + ) + ) + ); + return event; +} + +export function createQuoteTokenPriceFeedUpdated( + previousPriceFeed: Address, + newPriceFeed: Address, + logIndex: i32, + quoteToken: Address = QUOTE_TOKEN +): QuoteTokenPriceFeedUpdated { + const event = changetype(newMockEvent()); + configureEvent(event, logIndex); + event.parameters = new Array(); + event.parameters.push( + new ethereum.EventParam( + "quoteToken", + ethereum.Value.fromAddress(quoteToken) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "previousPriceFeed", + ethereum.Value.fromAddress(previousPriceFeed) + ) + ); + event.parameters.push( + new ethereum.EventParam( + "newPriceFeed", + ethereum.Value.fromAddress(newPriceFeed) ) ); return event;