From 81942c596707ecb73b7763e589f6eb1b00bf2b08 Mon Sep 17 00:00:00 2001 From: chrisli30 Date: Fri, 29 May 2026 18:41:22 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20BNB=20Smart=20Chain=20(56)=20support=20?= =?UTF-8?q?=E2=80=94=20AAVE=20V3=20/=20Uniswap=20V3=20/=20Chainlink=20/=20?= =?UTF-8?q?Superfluid=20/=20WBNB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Chains.BnbMainnet (56) plus per-protocol addresses across the five catalog modules whose protocols have meaningful BNB deployments. Every address was verified live via eth_getCode against a BNB RPC before merge. Per-protocol additions: aaveV3 pool 0x6807dc923806fE8Fd134338EABCA509979a7e0cB oracle 0x39bc1bfDa2130d6Bb6DBEfd366939b4c7aa7C697 (no wethGateway — native is BNB, not ETH) uniswapV3 swapRouter02 0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2 quoterV2 0x78D78E420Da98ad378D7799bE8f4AF69033EB077 factory 0xdB1d10011AD0Ff90774D0C6Bb92e5C5c8b4461F7 nonfungiblePositionManager 0x7b8A01B39D58278b5DE7e48c8449c9f4F5170613 universalRouter 0x4Dae2f939ACf50408e13d58534Ff8c2776d45265 permit2 0x000000000022D473030F116dDEE9F6B43aC78BA3 (same address everywhere) tokens.WETH WBNB (0xbb4C...95c) — see naming note below tokens.USDC Binance-Peg USDC (0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d) chainlink ethUsdFeed 0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e btcUsdFeed 0x264990fbd0A4796A3E3d8E37C4d5F87a3aCa5Ebf bnbUsdFeed 0x0567F2323251f0Aab15c8dFb1967E4e8A7D42aeE (new — BNB-only field for the chain's native-asset feed) wrapped weth WBNB (0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c) superfluid cfaForwarder 0xcfA132E353cB4E398080B9700609bb008eceB125 (same address as Ethereum / Base) Catalog-wide naming decision: `wrapped.weth[Chains.BnbMainnet]` and `uniswapV3.tokens.WETH[Chains.BnbMainnet]` both map to WBNB, not to Binance-Peg WETH (0x2170Ed0880ac9A755fd29B2688956BD959F933F8). The field name stays `weth` so chain-agnostic consumers can write `Protocols.wrapped.weth[chainId]` without per-chain branching; the semantic is "the canonical wrapper of the chain's native gas token," documented in both files. Templates that need bridged WETH on BNB specifically should pass that address inline. Catalog protocols deliberately NOT added on BNB (not deployed there): Aerodrome (Base-only), Compound V3 (probe failed at the address I expected — may not deploy on BNB at all; revisit when an official address surfaces), Ethena (sUSDe vault is L1-only), Frax Ether, Lido, Rocket Pool, Sky, Spark, Morpho Blue. Test changes: - "AAVE V3 covers the same chain set" invariant relaxed: Pool + Oracle must match chain sets, but WETH Gateway is now subset of Pool (gateway absent on BNB). - SwapRouter02 + Permit2 per-chain tests extended with BNB. - Pool-has-addresses-on-every-covered-chain test extended. - All 15 tests pass; no new tests needed for the additions themselves since the address-shape walk already covers them. Bumps to 0.2.0 (minor — new chain is additive, never patch). Downstream consumers (ava-sdk-js, context-memory, studio) pick up BNB by bumping the catalog dep; no code changes required in any of them since they all consume Protocols.X[chainId] generically. Operator-side prereqs verified separately and tracked in avs-infra/Adding_A_New_Chain.md's BNB Lessons subsection: - Arachnid CREATE2 deployer present (0x4e59...956C) - ERC-4337 EntryPoint v0.6/v0.7/v0.8 all canonical - Moralis natively supports BNB for token enrichment - Tenderly Simulation API requires a Request for BNB (chain 56 + 97 in the Request bucket of their Node column); workflows:simulate falls back to deterministic summary until Tenderly grants access. Request filing tracked separately. --- README.md | 10 +++++----- package.json | 2 +- src/chains.ts | 1 + src/protocols/aave-v3.ts | 6 ++++++ src/protocols/chainlink.ts | 12 ++++++++++++ src/protocols/superfluid.ts | 1 + src/protocols/uniswap-v3.ts | 24 ++++++++++++++++++++---- src/protocols/wrapped.ts | 19 +++++++++++++++---- tests/catalog.test.ts | 16 ++++++++++++---- 9 files changed, 73 insertions(+), 18 deletions(-) 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", () => {