diff --git a/README.md b/README.md index 6a729f8..322eb62 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ const sig = Protocols.aaveV3.eventTopics.Borrow; | Protocol | Contracts | Chains | |---|---|---| -| **AAVE V3** | Pool, Oracle, WETH Gateway + Pool methods/events ABI + topics | Mainnet, Sepolia, Base, Base Sepolia | +| **AAVE V3** | Pool, Oracle, WETH Gateway + Pool methods/events ABI + topics | Mainnet, Sepolia, Base, Base Sepolia, **BNB** (no WETH Gateway on BNB) | | **Aerodrome** | Router | Base | -| **Chainlink** | ETH/USD + BTC/USD feeds + AggregatorV3 ABI | Mainnet, Sepolia | +| **Chainlink** | ETH/USD + BTC/USD + BNB/USD feeds + AggregatorV3 ABI | Mainnet, Sepolia, **BNB** (BNB/USD is BNB-only) | | **Compound V3** | USDC Comet market | Mainnet, Base | | **Ethena** | USDe, sUSDe vault + custom (cooldown-aware) ABI | Mainnet | | **Frax Ether** | frxETH, sfrxETH vault + standard ERC-4626 ABI | Mainnet | @@ -34,9 +34,9 @@ const sig = Protocols.aaveV3.eventTopics.Borrow; | **Rocket Pool** | rETH + L1 burn/rate/value ABI | Mainnet, Base (bridged) | | **Sky (sDAI)** | sDAI vault + standard ERC-4626 ABI | Mainnet | | **Spark** | SparkLend Pool (AAVE V3 fork — reuse AAVE Pool ABI) | Mainnet | -| **Superfluid** | CFAv1Forwarder + setFlowrate/createFlow ABI | Mainnet, Base | -| **Uniswap V3** | SwapRouter02, QuoterV2, Permit2, Factory, NFT Position Manager, Universal Router + ABIs | Mainnet, Sepolia, Base, Base Sepolia | -| **Wrapped Ether** | WETH per chain + WETH9 ABI | Mainnet, Sepolia, Base, Base Sepolia | +| **Superfluid** | CFAv1Forwarder + setFlowrate/createFlow ABI | Mainnet, Base, **BNB** | +| **Uniswap V3** | SwapRouter02, QuoterV2, Permit2, Factory, NFT Position Manager, Universal Router + ABIs | Mainnet, Sepolia, Base, Base Sepolia, **BNB** | +| **Wrapped Ether** | Canonical wrapper of native gas + WETH9 ABI (WBNB on BNB) | Mainnet, Sepolia, Base, Base Sepolia, **BNB** | | **ERC-20** | Standard `approve` ABI fragment | n/a | Shared ABIs (consumed by multiple protocol modules): diff --git a/package.json b/package.json index e0986bd..5d5649d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@avaprotocol/protocols", - "version": "0.1.0", + "version": "0.2.0", "description": "Multi-chain catalog of DeFi protocol contract addresses, ABI fragments, and event topic hashes — covers AAVE V3, Uniswap V3, Chainlink, Lido, Morpho, and more.", "keywords": [ "defi", diff --git a/src/chains.ts b/src/chains.ts index ef7d083..fea0188 100644 --- a/src/chains.ts +++ b/src/chains.ts @@ -13,6 +13,7 @@ export const Chains = Object.freeze({ Holesky: 17_000 as const, BaseMainnet: 8453 as const, BaseSepolia: 84_532 as const, + BnbMainnet: 56 as const, }); export type ChainId = (typeof Chains)[keyof typeof Chains] | number; diff --git a/src/protocols/aave-v3.ts b/src/protocols/aave-v3.ts index 6eb7d0b..ca4b87a 100644 --- a/src/protocols/aave-v3.ts +++ b/src/protocols/aave-v3.ts @@ -24,6 +24,7 @@ const pool: AddressByChain = { [Chains.Sepolia]: "0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951", [Chains.BaseMainnet]: "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5", [Chains.BaseSepolia]: "0x8bAB6d1b75f19e9eD9fCe8b9BD338844fF79aE27", + [Chains.BnbMainnet]: "0x6807dc923806fE8Fd134338EABCA509979a7e0cB", }; /** @@ -36,12 +37,17 @@ const oracle: AddressByChain = { [Chains.Sepolia]: "0x2da88497588bf89281816106C7259e31AF45a663", [Chains.BaseMainnet]: "0x2Cc0Fc26eD4563A5ce5e8bdcfe1A2878676Ae156", [Chains.BaseSepolia]: "0x943b0dE18d4abf4eF02A85912F8fc07684C141dF", + [Chains.BnbMainnet]: "0x39bc1bfDa2130d6Bb6DBEfd366939b4c7aa7C697", }; /** * WETH gateway — wraps native ETH supplies so Pool can hold WETH as * the reserve. Templates that supply native ETH (rather than an * already-wrapped token) go through here instead of Pool directly. + * + * Only present on chains whose native gas token is ETH. Absent on + * BNB Chain (native is BNB; the equivalent there would be a "WBNB + * gateway" but AAVE V3 doesn't deploy one). */ const wethGateway: AddressByChain = { [Chains.EthereumMainnet]: "0xd01607c3C5eCABa394D8be377a08590149325722", diff --git a/src/protocols/chainlink.ts b/src/protocols/chainlink.ts index c9da447..a73d08d 100644 --- a/src/protocols/chainlink.ts +++ b/src/protocols/chainlink.ts @@ -16,15 +16,27 @@ import { type AddressByChain } from "./types"; const ethUsdFeed: AddressByChain = { [Chains.EthereumMainnet]: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419", [Chains.Sepolia]: "0x694AA1769357215DE4FAC081bf1f309aDC325306", + [Chains.BnbMainnet]: "0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e", }; const btcUsdFeed: AddressByChain = { [Chains.EthereumMainnet]: "0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c", + [Chains.BnbMainnet]: "0x264990fbd0A4796A3E3d8E37C4d5F87a3aCa5Ebf", +}; + +/** + * BNB/USD feed — only meaningful on BNB Chain (where BNB is the + * native gas token). The catalog ships this so templates on BNB can + * read native-asset prices the same way ETH-based chains read ETH/USD. + */ +const bnbUsdFeed: AddressByChain = { + [Chains.BnbMainnet]: "0x0567F2323251f0Aab15c8dFb1967E4e8A7D42aeE", }; export const chainlink = Object.freeze({ ethUsdFeed, btcUsdFeed, + bnbUsdFeed, /** Shared AggregatorV3 ABI — works for any Chainlink feed. */ aggregatorV3Abi, }); diff --git a/src/protocols/superfluid.ts b/src/protocols/superfluid.ts index 8e19d03..d0a32b8 100644 --- a/src/protocols/superfluid.ts +++ b/src/protocols/superfluid.ts @@ -8,6 +8,7 @@ import { type AbiFragment, type AddressByChain } from "./types"; const cfaForwarder: AddressByChain = { [Chains.EthereumMainnet]: "0xcfA132E353cB4E398080B9700609bb008eceB125", [Chains.BaseMainnet]: "0xcfA132E353cB4E398080B9700609bb008eceB125", + [Chains.BnbMainnet]: "0xcfA132E353cB4E398080B9700609bb008eceB125", }; /** CFAv1Forwarder minimal write surface — `setFlowrate` + `createFlow`. */ diff --git a/src/protocols/uniswap-v3.ts b/src/protocols/uniswap-v3.ts index 352eb1a..5a53e0d 100644 --- a/src/protocols/uniswap-v3.ts +++ b/src/protocols/uniswap-v3.ts @@ -21,6 +21,7 @@ const swapRouter02: AddressByChain = { [Chains.Sepolia]: "0x3bFA4769FB09eefC5a80d6E87c3B9C650f7Ae48E", [Chains.BaseMainnet]: "0x2626664c2603336E57B271c5C0b26F421741e481", [Chains.BaseSepolia]: "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4", + [Chains.BnbMainnet]: "0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2", }; /** QuoterV2 — off-chain quote helper for swap previews. */ @@ -29,6 +30,7 @@ const quoterV2: AddressByChain = { [Chains.Sepolia]: "0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3", [Chains.BaseMainnet]: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a", [Chains.BaseSepolia]: "0xC5290058841028F1614F3A6F0F5816cAd0df5E27", + [Chains.BnbMainnet]: "0x78D78E420Da98ad378D7799bE8f4AF69033EB077", }; /** @@ -40,6 +42,7 @@ const permit2: AddressByChain = { [Chains.Sepolia]: "0x000000000022d473030F116dDEE9F6B43aC78BA3", [Chains.BaseMainnet]: "0x000000000022d473030F116dDEE9F6B43aC78BA3", [Chains.BaseSepolia]: "0x000000000022d473030F116dDEE9F6B43aC78BA3", + [Chains.BnbMainnet]: "0x000000000022d473030F116dDEE9F6B43aC78BA3", }; /** Uniswap V3 Factory — derives the deterministic pool address per token-pair+fee. */ @@ -48,6 +51,7 @@ const factory: AddressByChain = { [Chains.Sepolia]: "0x0227628f3F023bb0B980b67D528571c95c6DaC1c", [Chains.BaseMainnet]: "0x33128a8fC17869897dcE68Ed026d694621f6FDfD", [Chains.BaseSepolia]: "0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24", + [Chains.BnbMainnet]: "0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7", }; /** NonfungiblePositionManager — LP NFT mint/burn/collect. */ @@ -56,6 +60,7 @@ const nonfungiblePositionManager: AddressByChain = { [Chains.Sepolia]: "0x1238536071E1c677A632429e3655c799b22cDA52", [Chains.BaseMainnet]: "0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1", [Chains.BaseSepolia]: "0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2", + [Chains.BnbMainnet]: "0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613", }; /** UniversalRouter — Uniswap's multi-step routing entrypoint (Permit2-aware). */ @@ -64,6 +69,7 @@ const universalRouter: AddressByChain = { [Chains.Sepolia]: "0x3A9D48AB9751398BbFa63ad67599Bb04e4BdF98b", [Chains.BaseMainnet]: "0x6ff5693b99212da76ad316178a184ab56d299b43", [Chains.BaseSepolia]: "0x492e6456d9528771018deb9e87ef7750ef184104", + [Chains.BnbMainnet]: "0x4Dae2f939ACf50408e13d58534Ff8c2776d45265", }; /** @@ -175,10 +181,18 @@ const factoryAbi: readonly AbiFragment[] = Object.freeze([ ]); /** - * Reference token addresses on the testnet markets these templates - * routinely target. Mainnet tokens vary per template — pass them - * inline rather than relying on a global registry the catalog - * doesn't yet maintain. + * Reference token addresses on the markets these templates routinely + * target. Mainnet tokens vary per template — pass them inline rather + * than relying on a global registry the catalog doesn't yet maintain. + * + * On chains whose native gas token is ETH, the `WETH` entry is the + * canonical WETH9-style contract. On BNB Chain, the native is BNB + * not ETH, so `WETH[BnbMainnet]` is **WBNB** (the canonical wrapper + * of the native token) — that's what Uniswap V3 pools on BNB actually + * quote against. Templates that specifically need the bridged-from- + * Ethereum WETH on BNB (`0x2170Ed0880ac9A755fd29B2688956BD959F933F8`) + * should pass it inline; the catalog leans on the "wrapper of native" + * semantic for consistency with `Protocols.wrapped.weth`. */ const tokens = Object.freeze({ WETH: { @@ -186,12 +200,14 @@ const tokens = Object.freeze({ [Chains.EthereumMainnet]: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", [Chains.BaseMainnet]: "0x4200000000000000000000000000000000000006", [Chains.BaseSepolia]: "0x4200000000000000000000000000000000000006", + [Chains.BnbMainnet]: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", } satisfies AddressByChain, USDC: { [Chains.Sepolia]: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238", [Chains.EthereumMainnet]: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", [Chains.BaseMainnet]: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", [Chains.BaseSepolia]: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + [Chains.BnbMainnet]: "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", } satisfies AddressByChain, }); diff --git a/src/protocols/wrapped.ts b/src/protocols/wrapped.ts index e6bcb62..2de7ff9 100644 --- a/src/protocols/wrapped.ts +++ b/src/protocols/wrapped.ts @@ -1,7 +1,16 @@ -// Wrapped Ether (WETH) — the canonical native-ETH wrapping contract. -// Mainnet WETH is a one-off; Base (and Base Sepolia) use the OP-stack -// predeploy at 0x4200…0006. Sepolia is the Uniswap-deployed test WETH -// used by AAVE Sepolia and Uniswap Sepolia. +// Wrapped native — the canonical wrapper of the chain's native gas +// token. On chains whose native is ETH (Mainnet, Sepolia, Base, Base +// Sepolia) this is the WETH9 (or OP-stack predeploy at 0x4200…0006) +// contract. On chains with a different native gas token, the field +// maps to the equivalent canonical wrapper — e.g. **WBNB** on BNB +// Chain. The field name stays `weth` so chain-agnostic consumers can +// write `Protocols.wrapped.weth[chainId]` without branching by chain. +// +// The bridged-from-Ethereum WETH on BNB +// (0x2170Ed0880ac9A755fd29B2688956BD959F933F8) is a different +// semantic — Binance-Peg ETH ERC-20, not the wrapped native — and is +// intentionally NOT mapped here. Templates that need it pass the +// address inline. import { Chains } from "../chains"; import { type AbiFragment, type AddressByChain } from "./types"; @@ -11,6 +20,8 @@ const weth: AddressByChain = { [Chains.Sepolia]: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", [Chains.BaseMainnet]: "0x4200000000000000000000000000000000000006", [Chains.BaseSepolia]: "0x4200000000000000000000000000000000000006", + // BNB Chain's native is BNB, not ETH — this entry is WBNB. + [Chains.BnbMainnet]: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", }; /** diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts index 2335ea1..480185a 100644 --- a/tests/catalog.test.ts +++ b/tests/catalog.test.ts @@ -87,11 +87,12 @@ describe("Event topic shape", () => { }); describe("AAVE V3 catalog", () => { - it("has Pool addresses on mainnet, sepolia, base, base-sepolia", () => { + it("has Pool addresses on every covered chain", () => { expect(Protocols.aaveV3.pool[Chains.EthereumMainnet]).toMatch(ADDRESS_RE); expect(Protocols.aaveV3.pool[Chains.Sepolia]).toMatch(ADDRESS_RE); expect(Protocols.aaveV3.pool[Chains.BaseMainnet]).toMatch(ADDRESS_RE); expect(Protocols.aaveV3.pool[Chains.BaseSepolia]).toMatch(ADDRESS_RE); + expect(Protocols.aaveV3.pool[Chains.BnbMainnet]).toMatch(ADDRESS_RE); }); it("ships the Pool method ABI with getUserAccountData + supply", () => { @@ -117,6 +118,7 @@ describe("Uniswap V3 catalog", () => { expect(Protocols.uniswapV3.swapRouter02[Chains.Sepolia]).toMatch(ADDRESS_RE); expect(Protocols.uniswapV3.swapRouter02[Chains.BaseMainnet]).toMatch(ADDRESS_RE); expect(Protocols.uniswapV3.swapRouter02[Chains.BaseSepolia]).toMatch(ADDRESS_RE); + expect(Protocols.uniswapV3.swapRouter02[Chains.BnbMainnet]).toMatch(ADDRESS_RE); }); it("ships exactInputSingle in the SwapRouter02 ABI", () => { @@ -132,6 +134,7 @@ describe("Uniswap V3 catalog", () => { Chains.Sepolia, Chains.BaseMainnet, Chains.BaseSepolia, + Chains.BnbMainnet, ]) { expect(Protocols.uniswapV3.permit2[chainId]?.toLowerCase()).toBe(expected.toLowerCase()); } @@ -165,12 +168,17 @@ describe("Shared ABIs", () => { }); describe("Chain coverage", () => { - it("AAVE V3 covers the same chain set on Pool/Oracle/WETH Gateway", () => { + it("AAVE V3 Pool + Oracle cover the same chains; WETH Gateway is a subset", () => { const poolChains = Object.keys(Protocols.aaveV3.pool).sort(); const oracleChains = Object.keys(Protocols.aaveV3.oracle).sort(); - const gatewayChains = Object.keys(Protocols.aaveV3.wethGateway).sort(); expect(oracleChains).toEqual(poolChains); - expect(gatewayChains).toEqual(poolChains); + // WETH Gateway is only deployed on chains whose native gas token + // is ETH. Chains without it (e.g. BNB Chain) still have Pool + + // Oracle. So the invariant is "gateway ⊆ pool", not equality. + const gatewayChains = Object.keys(Protocols.aaveV3.wethGateway); + for (const cid of gatewayChains) { + expect(poolChains).toContain(cid); + } }); it("Uniswap V3 covers the same chain set across its contracts", () => {