Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/flows/chain-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ No dedicated sequence diagram for this flow. The architecture diagram in the roo
## Steps

1. **protocol-core — chain config.** Create `packages/protocol-core/src/chains/<chain>.ts`
exporting a `ChainConfig` with chain id, name, native token, wrapped native, supported
protocol versions, schema variant, and stablecoin list.
exporting a `ChainConfig` with chain id, name, native symbol, wrapped native, supported
protocol versions, and stablecoin list. Add the chain-level `multicall` address and one
`deployments` entry per protocol version the chain runs. Both fields are optional, but a
chain without `deployments` resolves to `undefined` from `getDeployment`.
2. **protocol-core — registry.** Import and register the new config in
`packages/protocol-core/src/chains/registry.ts` (both `_registry` and `CHAIN_ID`).
3. **protocol-core — barrel.** Re-export the new chain config from
Expand All @@ -26,7 +28,9 @@ No dedicated sequence diagram for this flow. The architecture diagram in the roo

## Inputs

- Chain id, factory deployment address, native + wrapped native metadata, stablecoin list.
- Chain id, native + wrapped native metadata, stablecoin list, multicall address, and the
contract addresses each protocol version deploys — factory and swap router for every family,
plus quoter, position manager and pool deployer where that family declares them.

## Outputs

Expand Down
21 changes: 21 additions & 0 deletions packages/protocol-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,32 @@ const stables = getStablecoins(137)
const feeAmount = computeV2Fee(1_000_000n) // V2 swap fee in raw units
```

## Contract deployments

A chain config may carry a version-agnostic `multicall` address and a `deployments`
list. Each entry is discriminated by protocol version, so `getDeployment` returns
exactly the contracts that family deploys.

```ts
import { getDeployment, PROTOCOL_VERSIONS } from '@quickswap-defi/protocol-core'

const algebra = getDeployment(137, PROTOCOL_VERSIONS.V3)
algebra?.poolDeployer // Algebra derives pools from a pool deployer

const uniswapFork = getDeployment(169, PROTOCOL_VERSIONS.UNIV3)
uniswapFork?.factory // the Uniswap-V3 fork derives pools from the factory
```

Both fields are optional: a chain config without them is still valid, and
`getDeployment` yields `undefined` for any version a chain does not deploy.

## Public API

- **Registry** — `CHAIN_REGISTRY`, `CHAIN_ID`, `getChain`, `getChainOrThrow`, `getSupportedChainIds`
- **Deployments** — `getDeployment`
- **Per-chain configs** — `POLYGON`, `BASE`, `MANTRA`, `MANTA`, `SONEIUM`, `SOMNIA`, `IMX`, `XLAYER`, `ETHEREUM`
- **Types** — `ChainConfig`, `ChainProtocolEntry`, `ProtocolVersion`, `SchemaVariant`, `TokenInfo`
- **Deployment types** — `ProtocolDeployment`, `V2Deployment`, `V3Deployment`, `V4Deployment`, `UniV3Deployment`, `DeploymentFor`
- **Constants** — `PROTOCOL_VERSIONS`, `SCHEMA_VARIANTS`
- **Protocol helpers** — `getSchemaVariant`, `getSupportedVersions`, `getProtocolVersionLabel`
- **Fees** — `V2_FEE_BPS`, `V2_FEE_RATE`, `computeV2Fee`
Expand Down
5 changes: 4 additions & 1 deletion packages/protocol-core/scripts/validate-addresses.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { keccak_256 } from '@noble/hashes/sha3'
import { getSupportedChainIds, getChain } from '../src/chains/registry'
import { checkDeploymentCoherence } from '../src/chains/deploymentCoherence'

function toChecksumAddress(address: string): string {
const addr = address.toLowerCase().replace('0x', '')
Expand Down Expand Up @@ -68,10 +69,12 @@ for (const chainId of chainIds) {
)
}
}

errors.push(...checkDeploymentCoherence(chain))
}

if (errors.length > 0) {
console.error(`\n❌ ${errors.length} invalid EIP-55 checksum(s) found:\n`)
console.error(`\n❌ ${errors.length} address validation error(s) found:\n`)
errors.forEach((e) => console.error(` • ${e}`))
console.error('')
process.exit(1)
Expand Down
94 changes: 94 additions & 0 deletions packages/protocol-core/src/__tests__/chains/chain-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { SOMNIA } from '../../chains/somnia'
import { IMX } from '../../chains/imx'
import { XLAYER } from '../../chains/xlayer'
import { ETHEREUM } from '../../chains/ethereum'
import { CHAIN_REGISTRY } from '../../chains/registry'
import { checkDeploymentCoherence } from '../../chains/deploymentCoherence'
import type { ChainConfig } from '../../chains/types'

const ALL_CHAINS: ChainConfig[] = [
Expand All @@ -22,6 +24,23 @@ const ALL_CHAINS: ChainConfig[] = [
ETHEREUM,
]

const CHAINS_WITH_DEPLOYMENTS = ALL_CHAINS.filter((chain) => chain.deployments !== undefined)

const CHAINS_WITH_MULTICALL = ALL_CHAINS.filter((chain) => chain.multicall !== undefined)

const UNISWAP_FORK_ADDRESSES = [
{
chain: MANTA,
factory: '0x56c2162254b0E4417288786eE402c2B41d4e181e',
positionManager: '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff',
},
{
chain: IMX,
factory: '0x56c2162254b0E4417288786eE402c2B41d4e181e',
positionManager: '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff',
},
]

describe('The chain configuration data', () => {
describe('basic fields', () => {
it.each(ALL_CHAINS)('$name has positive chainId', (chain) => {
Expand Down Expand Up @@ -119,6 +138,81 @@ describe('The chain configuration data', () => {
})
})

describe('The deployment fixtures', () => {
it('populates deployment data on more than one chain', () => {
expect(CHAINS_WITH_DEPLOYMENTS.length).toBeGreaterThan(1)
})

it('lists the Polygon entries in the order its protocols declare them', () => {
const declaredOrder = POLYGON.protocols.map((protocol) => protocol.version)
const deployedOrder = POLYGON.deployments!.map((deployment) => deployment.version)

expect(deployedOrder).toEqual(declaredOrder)
})

it.each([MANTA, IMX])('gives $name the single Uniswap-V3 fork entry it declares', (chain) => {
const versions = chain.deployments!.map((deployment) => deployment.version)

expect(versions).toEqual(['univ3'])
})

it('limits the Polygon v2 entry to the contracts that family deploys', () => {
const v2 = POLYGON.deployments!.find((deployment) => deployment.version === 'v2')!

expect(Object.keys(v2).sort()).toEqual(['factory', 'swapRouter', 'version'])
})

it('gives the Polygon Algebra entry a pool deployer', () => {
const v3 = POLYGON.deployments!.find((deployment) => deployment.version === 'v3')!

expect(v3).toHaveProperty('poolDeployer')
})

it.each([MANTA, IMX])('leaves the $name entry without a pool deployer', (chain) => {
const univ3 = chain.deployments!.find((deployment) => deployment.version === 'univ3')!

expect(Object.keys(univ3)).not.toContain('poolDeployer')
})

// Manta Pacific and Immutable zkEVM deploy the same factory and the same
// position manager. Both values match the contracts deployed on each chain.
it.each(UNISWAP_FORK_ADDRESSES)(
'pins the $chain.name factory and position manager',
({ chain, factory, positionManager }) => {
const univ3 = chain.deployments!.find((deployment) => deployment.version === 'univ3')!

expect(univ3.factory).toBe(factory)
expect(univ3).toHaveProperty('positionManager', positionManager)
},
)
})

describe('The multicall addresses', () => {
it('populates a multicall address on more than one chain', () => {
expect(CHAINS_WITH_MULTICALL.length).toBeGreaterThan(1)
})

it.each(CHAINS_WITH_MULTICALL)('gives $name a multicall address', (chain) => {
expect(chain.multicall).toMatch(/^0x[0-9a-fA-F]{40}$/)
})
})

describe('The registry deployment coherence', () => {
const REGISTERED_CHAINS = Object.values(CHAIN_REGISTRY)

it('exercises the coherence rules against real deployment data', () => {
const carrying = REGISTERED_CHAINS.filter((chain) => chain.deployments !== undefined)

expect(carrying.length).toBeGreaterThan(1)
})

it.each(REGISTERED_CHAINS)('keeps $name coherent with the protocols it declares', (chain) => {
const violations = checkDeploymentCoherence(chain)

expect(violations).toEqual([])
})
})

describe('Ethereum stablecoins', () => {
it('Ethereum includes USDC', () => {
const symbols = ETHEREUM.stablecoins.map((s) => s.symbol)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { describe, it, expect } from 'vitest'
import { checkDeploymentCoherence } from '../../chains/deploymentCoherence'
import {
PROTOCOL_VERSIONS,
type ChainConfig,
type ProtocolDeployment,
} from '../../chains/types'

const FACTORY = '0x411b0fAcC3489691f28ad58c47006AF5E3Ab3A28'
const ROUTER = '0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff'
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const NATIVE_SENTINEL_LOWERCASE = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
const NATIVE_SENTINEL_CHECKSUMMED = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'

function buildChain(overrides: Partial<ChainConfig> = {}): ChainConfig {
return {
chainId: 137,
name: 'Test Chain',
nativeSymbol: 'POL',
wrappedNative: { address: FACTORY, symbol: 'WPOL', decimals: 18 },
protocols: [{ version: PROTOCOL_VERSIONS.V2, exposeDynamicFee: false }],
stablecoins: [],
...overrides,
}
}

const COHERENT_V2_ENTRY = {
version: PROTOCOL_VERSIONS.V2,
factory: FACTORY,
swapRouter: ROUTER,
} as const

/** One entry per family with every address slot filled by a sentinel. */
const ALL_SENTINEL_ENTRIES: ReadonlyArray<ProtocolDeployment> = [
{ version: PROTOCOL_VERSIONS.V2, factory: ZERO_ADDRESS, swapRouter: ZERO_ADDRESS },
{
version: PROTOCOL_VERSIONS.V3,
factory: ZERO_ADDRESS,
swapRouter: ZERO_ADDRESS,
quoter: ZERO_ADDRESS,
positionManager: ZERO_ADDRESS,
poolDeployer: ZERO_ADDRESS,
},
{
version: PROTOCOL_VERSIONS.V4,
factory: ZERO_ADDRESS,
swapRouter: ZERO_ADDRESS,
quoter: ZERO_ADDRESS,
positionManager: ZERO_ADDRESS,
poolDeployer: ZERO_ADDRESS,
},
{
version: PROTOCOL_VERSIONS.UNIV3,
factory: ZERO_ADDRESS,
swapRouter: ZERO_ADDRESS,
quoter: ZERO_ADDRESS,
positionManager: ZERO_ADDRESS,
},
]

describe('The deployment coherence check', () => {
describe('when a chain is coherent', () => {
it('accepts a chain that declares no deployments', () => {
const chain = buildChain()

expect(checkDeploymentCoherence(chain)).toEqual([])
})

it('accepts a deployment whose version the chain declares', () => {
const chain = buildChain({ deployments: [COHERENT_V2_ENTRY] })

expect(checkDeploymentCoherence(chain)).toEqual([])
})

it('accepts a real multicall address', () => {
const chain = buildChain({ multicall: ROUTER })

expect(checkDeploymentCoherence(chain)).toEqual([])
})
})

describe('when a deployment contradicts the chain', () => {
it('rejects a version the chain never declares', () => {
const chain = buildChain({
deployments: [
{
version: PROTOCOL_VERSIONS.V3,
factory: FACTORY,
swapRouter: ROUTER,
quoter: FACTORY,
positionManager: ROUTER,
poolDeployer: FACTORY,
},
],
})

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(1)
expect(violations[0]).toContain('Test Chain (137)')
expect(violations[0]).toContain('deployments[0]')
expect(violations[0]).toContain(PROTOCOL_VERSIONS.V3)
})

it('rejects the same version declared twice on one chain', () => {
const chain = buildChain({ deployments: [COHERENT_V2_ENTRY, COHERENT_V2_ENTRY] })

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(1)
expect(violations[0]).toContain('deployments[1]')
expect(violations[0]).toContain(PROTOCOL_VERSIONS.V2)
})
})

describe('when an address slot holds a placeholder', () => {
it('rejects the all-zero address and names the slot that holds it', () => {
const chain = buildChain({
deployments: [{ version: PROTOCOL_VERSIONS.V2, factory: ZERO_ADDRESS, swapRouter: ROUTER }],
})

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(1)
expect(violations[0]).toContain('deployments[0].factory')
expect(violations[0]).toContain(ZERO_ADDRESS)
})

it.each([NATIVE_SENTINEL_LOWERCASE, NATIVE_SENTINEL_CHECKSUMMED])(
'rejects the native-token placeholder written as %s',
(sentinel) => {
const chain = buildChain({
deployments: [{ version: PROTOCOL_VERSIONS.V2, factory: FACTORY, swapRouter: sentinel }],
})

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(1)
expect(violations[0]).toContain('deployments[0].swapRouter')
},
)

it('rejects a placeholder multicall address', () => {
const chain = buildChain({ multicall: ZERO_ADDRESS })

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(1)
expect(violations[0]).toContain('multicall')
expect(violations[0]).toContain(ZERO_ADDRESS)
})

it('rejects a placeholder multicall even when the chain declares no deployments', () => {
const chain = buildChain({ multicall: NATIVE_SENTINEL_CHECKSUMMED, deployments: undefined })

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(1)
expect(violations[0]).toContain('multicall')
})

it.each(ALL_SENTINEL_ENTRIES)(
'inspects every address slot the $version family declares',
(entry) => {
const chain = buildChain({
protocols: [{ version: entry.version, exposeDynamicFee: false }],
deployments: [entry],
})
const addressSlotCount = Object.keys(entry).length - 1

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(addressSlotCount)
},
)
})

describe('when a chain breaks several rules at once', () => {
it('reports one message per violation', () => {
const chain = buildChain({
multicall: ZERO_ADDRESS,
deployments: [
COHERENT_V2_ENTRY,
{ version: PROTOCOL_VERSIONS.V2, factory: ZERO_ADDRESS, swapRouter: ROUTER },
],
})

const violations = checkDeploymentCoherence(chain)

expect(violations).toHaveLength(3)
})
})
})
12 changes: 12 additions & 0 deletions packages/protocol-core/src/__tests__/chains/deployments.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, it, expectTypeOf } from 'vitest'
import { getDeployment } from '../../chains/deployments'
import { PROTOCOL_VERSIONS, type V3Deployment, type UniV3Deployment } from '../../chains/types'

describe('The deployment lookup', () => {
it('narrows its result to the family of the version it was given', () => {
expectTypeOf(getDeployment(137, PROTOCOL_VERSIONS.V3)).toEqualTypeOf<V3Deployment | undefined>()
expectTypeOf(getDeployment(169, PROTOCOL_VERSIONS.UNIV3)).toEqualTypeOf<
UniV3Deployment | undefined
>()
})
})
Loading