Skip to content

feat: price swETH from its own exchange rate - #198

Merged
zmanian merged 1 commit into
mainfrom
fix/sweth-rate-provider-extension
Sep 18, 2026
Merged

zmanian merged 1 commit into
mainfrom
fix/sweth-rate-provider-extension

Conversation

@zmanian

@zmanian zmanian commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Why

Redstone abandoned the swETH adapter this PriceRouter prices through; its last update was 2025-05-30. Every cellar holding swETH now reverts in totalAssets() with RedstoneEthPriceFeedExtension__STALE_PRICE() (0x2cfb7afb), so Turbo swETH (0xd33dAd974b938744dAC81fE00ac67cb5AA13958E) is frozen.

Part of the wider remediation for the withdrawal outage. rsETH, ezETH and weETH have the same Redstone problem, but each has a live Chainlink feed, so they are being repointed with existing code. swETH has no Chainlink feed, and all three swETH/WETH Uniswap v3 pools are too thin for a TWAP: the 0.01% pool that ChangingSwethPricing.t.sol (Macro audit 9) priced swETH against in 2023 now holds 0.003 swETH, the 0.3% pool is empty, and the deepest, 0.05%, holds 0.62 swETH. It needs this extension.

What

SwEthExtension prices swETH from its own ETH exchange rate. swETH exposes the IRateProvider getRate() interface, and Swell is still actively repricing it.

It is a line-for-line derivation of the audited weEthExtension, with weETH replaced by swETH. After normalising the renames (contract name, error names, token name and address), the executable code is identical to the original. That was checked mechanically by diffing the two files, not by reading them.

It is deliberately fixed to swETH rather than a generic rate-provider extension. A generic version takes an arbitrary address and function selector, which gives up the compile-time guarantee that makes the audited original safe, and only one asset needs it.

Tests

Mirrors the existing eETHExtension.t.sol suite:

  • testAddSwEthExtension — prices swETH against getRate()
  • testUsingExtensionWithWrongAsset — the asset guard
  • testAddingSwEthWithoutPricingWeth — the WETH dependency

Plus one integration test the originals don't have:

  • testRepointingSwEthUnfreezesTurboSwEth — runs against the live PriceRouter, through the same owner-only, 7-day-timelocked startEditAsset / completeEditAsset edit the Safe will execute, and asserts Turbo swETH values again (~2.26 WETH). The timelock is simulated by setting the edit's due time rather than warping, because warping would make the live WETH/USD feed look stale and fail for the wrong reason.

Written test-first: the suite failed because the contract did not exist, then passed. Mutation-tested, meaning the code was broken on purpose to confirm the tests notice. A 10x price error fails both pricing tests through the router's own BadAnswer guard, and removing the asset guard fails the wrong-asset test.

Verification notes

  • 4/4 pass locally, on this branch off main.
  • The full repo builds cleanly, before and after.
  • Not run locally: the rest of the suite. It pins historical blocks and needs an archive RPC, and no free archive endpoint would serve it.
  • CI is red for an unrelated, pre-existing reason. All 144 failures are the same setUp() error — Could not instantiate forked environment with fork url — meaning CI's archive RPC (the ALCHEMY_API_KEY secret) can't create forks. Every CI run in this repo has failed this way since at least 2026-08-20, including all runs on feat: keeper-driven share price oracle for networks without Chainlink Automation #197. No test ran an assertion, so CI provides no signal on this change either way. The key needs rotating.
  • The new tests pin block 26007554 and so also need an archive-capable MAINNET_RPC_URL, like the rest of the suite.

Deployment

Deployed at 0xad84a81157fe6804fc8e13658452a60793d123fb via the canonical CREATE2 deployer at salt 0, from this branch's head 852c3a0 (tx 0x4014fbe9f3d70296d74f4db8efa7f8b1e89b4ff45caedcd4a947eebb354caff6). The address commits to the exact initcode, so anyone can reproduce it from source. priceRouter() returns the live router and getPriceInUSD(swETH) returns $2,960.93.

The contract alone changes no pricing. To use it, the PriceRouter owner (the 4-of-7 Safe 0x7340D1FeCD4B64A4ac34f826B21c945d44d7407F) would deploy the extension and run the timelocked startEditAsset(swETH, (3, <extension>), ""), then completeEditAsset seven days later. cancelEditAsset can withdraw a pending edit.

Generated with Claude Code

Redstone abandoned the swETH adapter this router prices through — its last
update was 2025-05-30 — so every cellar holding swETH reverts in
totalAssets() with RedstoneEthPriceFeedExtension__STALE_PRICE. Turbo swETH is
frozen as a result.

Chainlink has no swETH feed, and the only Uniswap v3 pool holds about 0.5
swETH, far too thin for a TWAP. swETH does expose its own ETH exchange rate
through the IRateProvider getRate() interface, and Swell is still actively
repricing it.

SwEthExtension is a line-for-line derivation of the audited weEthExtension
with weETH replaced by swETH. After normalising the renames (contract name,
error names, token identifier and address) the executable code is identical
to the original, verified mechanically by diff. It is deliberately asset-bound
rather than a generic rate-provider extension: a generic version takes an
arbitrary address and selector, which removes exactly the compile-time
guarantee that makes the audited original safe, and only one asset needs it.

Tests mirror the existing eETH extension suite — pricing against getRate(),
the wrong-asset guard, and the missing-WETH dependency — plus one integration
test against the live PriceRouter. That test performs the same owner-only,
timelocked edit the Safe will execute and asserts Turbo swETH values again
(~2.26 WETH). The 7-day timelock is simulated by setting the edit's due-time
rather than warping, since warping would stale the live WETH/USD feed.

Mutation-tested: a 10x price error fails both pricing tests via the router's
own BadAnswer guard, and removing the asset guard fails the wrong-asset test.

Tests pin block 26007554 and so need an archive-capable MAINNET_RPC_URL, as
the rest of this suite does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 656bfa6f-0e91-4c22-850d-dec75aced953

📥 Commits

Reviewing files that changed from the base of the PR and between 6d25401 and 852c3a0.

📒 Files selected for processing (2)
  • src/modules/price-router/Extensions/Swell/SwEthExtension.sol
  • test/testPriceRouter/SwEthExtension.t.sol
 __________________________________________________________________________
< Your code is like a magic trick: now you see it... now it's `undefined`. >
 --------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@zmanian
zmanian merged commit 07efdfa into main Sep 18, 2026
1 of 2 checks passed
@Elsreth

Elsreth commented Sep 21, 2026

Copy link
Copy Markdown

Hi, I am trying to withdraw my funds from Turbo sweth vault but I get an error whenever I call redeem function. If that proposal is signed and deployed will I be able to redeem?

@zmanian

zmanian commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

yes the admin multisig needs to approve the transactions and then you'll be able to redeem

@Elsreth

Elsreth commented Sep 22, 2026

Copy link
Copy Markdown

I can see only 2 signed, do you think there's any chance we can get the other 2?

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