Skip to content

Add Arbitrum Uniswap V3 WBTC-cbBTC CLM#1797

Open
KaiCode2 wants to merge 2 commits into
beefyfinance:masterfrom
KaiCode2:feat/clm-arb-cbbtc-wbtc
Open

Add Arbitrum Uniswap V3 WBTC-cbBTC CLM#1797
KaiCode2 wants to merge 2 commits into
beefyfinance:masterfrom
KaiCode2:feat/clm-arb-cbbtc-wbtc

Conversation

@KaiCode2
Copy link
Copy Markdown
Contributor

@KaiCode2 KaiCode2 commented May 14, 2026

New token: cbBTC on Arbitrum
Token address: 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf
Pool: WBTC/cbBTC Uniswap V3 0.01% — 0x9B42809aaaE8d088eE01FE637E948784730F0386

Per @'s feedback (thanks!), this revision uses an Arbitrum-specific arbcbBTC oracleId so cbBTC on Arbitrum is priced independently from cbBTC on other chains, and the on-chain oracle reads the cbBTC/WBTC UniV3 pool TWAP rather than the Chainlink BTC/USD feed. Pool observation cardinality has been bumped to 360.

CLM deployment on Arbitrum mainnet:

Contract Address
Vault 0x618D9328EB1D06dFfD2CB4B67523b2cB5740e32f
Strategy 0x2a25771642EaBD9EBEa7245d578E5D813D5301e7
Reward pool 0x6B29227D91F1317bb945FbCC87B37ef39d04C5f8

Strategy params: positionWidth=20, maxTickDeviation=2, twapInterval=120. Vault owner = 0x9A94…BdaD (vaultOwner), strategy owner = 0x6d28afD…D89F (strategyOwner), reward pool owner = 0xf7EC…CCcd (devMultisig). Strategist = my ledger.


Pool observation cardinality bump (done — no keeper action needed)

pool.increaseObservationCardinalityNext(360) was called from the deployer wallet to support a 360-second TWAP on the new oracle wiring (Beefy's default for CLMs). Tx: 0x58acbd28bc459236b8f6f52cf001fa4086c46de8fe91f7370e9020da8fb248a4. observationCardinalityNext is now 360 (current observationCardinality grows organically as the pool gets used).


Pre-activation wiring required (keeper multisig 0x4fED5491693007f0CD49f4614FFC38Ab6A04B619 to execute on Arbitrum):

cbBTC needs to be wired into Beefy oracle (UniV3 pool TWAP) and into BeefySwapper so harvest can convert cbBTC fees to WETH.

setOracle on BeefyOracle 0x5C7c7Bb0c9251821cB5a1D9c08F21B0DAD5efe65:

0x6d64a420000000000000000000000000cbb7c0000ab88b473b1f5afd9ef808440eed33bf0000000000000000000000003ea93706827c7009604a7cad51622ad99387869000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000020000000000000000000000002f2a2543b76a4166549f7aab2e75bef0aefc5b0f000000000000000000000000cbb7c0000ab88b473b1f5afd9ef808440eed33bf00000000000000000000000000000000000000000000000000000000000000010000000000000000000000009b42809aaae8d088ee01fe637e948784730f038600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000168

Decoded: setOracle(cbBTC, beefyOracleUniswapV3 0x3EA93706…, abi.encode([WBTC, cbBTC], [0x9b42…pool], [360s TWAP])) — prices cbBTC against WBTC using the same pool we're vaulting (the deepest cbBTC liquidity on Arbitrum), with a 360-second TWAP.

setSwapInfo on BeefySwapper 0xCee843CD04E3758dDC5BCFf08647DddB117151D0:

0xb84ea773000000000000000000000000cbb7c0000ab88b473b1f5afd9ef808440eed33bf00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000e592427a0aece92de3edee1f18e0157c0586156400000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000008400000000000000000000000000000000000000000000000000000000000000a400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000144c04b8d59000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000cee843cd04e3758ddc5bcff08647dddb117151d0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042cbb7c0000ab88b473b1f5afd9ef808440eed33bf0000642f2a2543b76a4166549f7aab2e75bef0aefc5b0f0001f482af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Decoded: cbBTC → WETH via Uniswap V3 SwapRouter (cbBTC → WBTC at 0.01% → WETH at 0.05%), amountIndex=132, minIndex=164, minAmountSign=0.

Both calldata strings were fork-validated end-to-end (impersonated keeper on Arbitrum fork, applied both, then harvested with simulated swap volume — fees flowed correctly to caller / Beefy fee recipient / strategist).


Mainnet lifecycle test (deployer 0x2cF7…be4d, all phases executed on Arbitrum):

Phase Tx
Deposit (~$170 WBTC + ~$160 cbBTC → 7006 shares) 0x4108d4e…44766
strategy.panic(0,0) (paused=true, LP emptied to idle) 0x84ce5cf…f0b2e
vault.withdrawAll(0,0) (7006 shares → 3606 WBTC + 3411 cbBTC, ≈ 87.5% of vault) 0x12b342e…17699
strategy.unpause() (paused=false, isCalm=true, burn-share backing redeployed) 0xea58aec…fde22
Redeposit (6801 shares minted, positions rebuilt) 0xfd75d6b…89a5e
Strategy ownership → Beefy strategyOwner (after lifecycle test, as required) 0x7ff5b1c…5cf8f
Pool observation cardinality bump (post-PR-review feedback) 0x58acbd28…48a4

Final vault state: 6801 deployer shares (of 7801 total — 1000 are the standard MINIMUM_LIQUIDITY burn shares), pool holds 3907 WBTC + 3892 cbBTC across main+alt, isCalm=true, paused=false.

Local validators pass: npm run prettier --check, npm run test:ts, npm run checksum (packages/address-book), npm run checkClms.

Front-end PR: beefyfinance/beefy-v2#3138

@iamjackgale
Copy link
Copy Markdown
Member

Awesome, thanks @KaiCode2. Spotted a few minor things:

  • Please can you set the observations for the UniV3 LP to 360? Needs sufficient for our calmness check, and 360 is the default standard for all CLMs.

  • I'd recommend setting a separate local price source for cbBTC on Arbitrum, rather than relying on the existing prices on other chains, just because these can diverge. I would usually add the price source into fetchConcentratedLiquidityTokenPrices.ts and assign a new oracleId for the chain like arbcbBTC. You'll then need to populate with that oracleId in beefyCowVaults.json and in the addressbook. Here's an example addition for fetching the price; note the firstToken/secondToken is in the opposite order to LP token0/token1.

    {
      type: 'UniV3',
      oracleId: 'arbcbBTC',
      decimalDelta: 1,
      pool: '0x9b42809aaae8d088ee01fe637e948784730f0386',
      firstToken: 'WBTC',
      secondToken: 'arbcbBTC',
    },
  • I'd also recommend setting the oracle to cbBTC directly rather than using Chainlink's BTC feed... just in case there's ever some issue with cbBTC. Ideally use the biggest source of onchain liquidity on Arbitrum. Looks like the LP you're vaulting is the best one. Lmk if you need a hand with this.

  • Still need to test the strategy harvests properly after the routes + oracle are set. But route looked good to me.

Looks sensible otherwise. I would note though that CLMs don't tend to do especially well for very-correlated 1-tick LPs. This is just because most depositors are conscious that the price shouldn't move much, so they're happy to manage their own LPs at narrow ranges, and Beefy doesn't add much additional value. Just to manage expectations 😊

@KaiCode2
Copy link
Copy Markdown
Contributor Author

Thanks for the thoughtful review! Addressed all of it in commit a94cb72:

  1. Cardinality bump to 360 — done on-chain ahead of the rest, since the new oracle TWAP depends on it. Tx 0x58acbd2…48a4. observationCardinalityNext is now 360; current observationCardinality will grow organically as the pool gets used.

  2. Separate arbcbBTC oracleId — applied across three files exactly as you suggested:

    • packages/address-book/src/address-book/arbitrum/tokens/tokens.ts: cbBTC entry oracleId changed from 'cbBTC' to 'arbcbBTC'
    • src/data/arbitrum/beefyCowVaults.json: tokenOracleIds updated to ["WBTC", "arbcbBTC"]
    • src/utils/fetchConcentratedLiquidityTokenPrices.ts: added the entry you pasted (cbBTC/WBTC pool, firstToken: 'WBTC', secondToken: 'arbcbBTC')
  3. Oracle source switched to the cbBTC/WBTC UniV3 pool TWAP (not Chainlink BTC/USD). The updated setOracle calldata in the PR description above now targets beefyOracleUniswapV3 (0x3EA9370…78690) with route=[WBTC, cbBTC], pools=[0x9b42…0386], twapPeriods=[360]. setSwapInfo is unchanged.

  4. Harvest re-verified with the new wiring — re-ran the fork test (SimulateHarvestArbCbBtcWbtc) impersonating the keeper, applying the new setOracle calldata + setSwapInfo, driving real swap volume through the pool, then harvesting. All three recipients (caller / Beefy fee recipient / strategist) receive their splits cleanly. Total fees charged in this run: ~$3.50 of WETH for ~$875k of round-trip volume on a ~$170k LP share.

  5. Noted on CLMs not adding much value for very-correlated 1-tick LPs — appreciate the heads-up. We're at positionWidth=20 (≈ ±0.2%), which is narrow but not the absolute minimum, so hopefully there's some headroom for users who don't want to actively manage. Happy to widen if you prefer.

Front-end PR (beefyfinance/beefy-v2#3138) will get a follow-up commit bumping the addressbook dependency once this one lands and a new version is published.

@KaiCode2 KaiCode2 force-pushed the feat/clm-arb-cbbtc-wbtc branch from a94cb72 to 7d2b305 Compare May 22, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants