Skip to content

feat: #2391 add ether.fi liquid restaking protocol integration - #2423

Open
zkasuran wants to merge 3 commits into
KeeperHub:stagingfrom
zkasuran:feat/ether-fi
Open

feat: #2391 add ether.fi liquid restaking protocol integration#2423
zkasuran wants to merge 3 commits into
KeeperHub:stagingfrom
zkasuran:feat/ether-fi

Conversation

@zkasuran

Copy link
Copy Markdown

Issue

Closes #2391

What this changes

Adds ether.fi as a new ABI-driven protocol. Depositing ETH into the Liquidity Pool mints eETH, a rebasing receipt earning staking plus EigenLayer restaking rewards; eETH wraps into weETH, the non-rebasing form most DeFi integrations use.

13 actions across three mainnet contracts: stake, wrap and unwrap as writes, plus ten reads covering pool accounting (getTotalPooledEther, amountForShare, sharesForAmount), the wrapper rate (getRate, getWeETHByeETH, getEETHByWeETH), balances and supply.

Contracts, verified on-chain before the addresses were written down: Liquidity Pool 0x308861A430be4cce5502d0A12724771Fc6DaF216, eETH 0x35fA164735182de50811E8e2E824cFb9B6118ac2, weETH 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee. The date and the derivation are in a comment on the definition per specs/protocol-coverage-methodology.md:303-304.

Beyond the definition, ABIs and unit test:

  • docs/plugins/ether-fi.md, its _meta.ts key and an overview.md row
  • tests/integration/protocol-ether-fi-onchain.test.ts, 12 cases
  • tests/unit/__goldens__/protocol-calldata/ether-fi.json
  • public/protocols/ether-fi.png, the weETH mark from the Optimism token list rendered to 256x256
  • unit cases for chain exclusion, EIP-55 checksums and expectation-field resolution

wrap and unwrap stay skipped with a reason, as planned. lib/test-data/chain-test-data.ts is untouched, so no eETH entry was added to the shared token registry. No coverage runner, so nothing to register in a shard list.

Two encoding fixes worth reviewing closely

Unnamed ABI outputs. Every output was declared "name": "", so structureAbiOutputs returns the bare scalar and a field: expectation cannot resolve against it. All 13 are now named, with the overrides keyed to those names instead of result.

Bindings keyed to renamed inputs. This one was silent and is the reason the goldens changed shape. The overrides rename the raw ABI parameters for readability (_share to shares, _amount to ethAmount, _eETHAmount and _weETHAmount to amount), but the testData still bound the old names. An unmatched binding key is dropped rather than rejected, then the encoder falls back to a type-derived default, so all four conversion reads were encoding 1 wei where the fixture said 1 ETH. The goldens now carry 0de0b6b3a7640000.

Because nothing errors when that happens, there is now a unit case asserting every binding key names a declared input. Reverting one key back to _share fails it with the offending key and the valid set.

Scope

One change. The definition, its three ABIs, the golden keyed off its testData, the docs page describing its actions and the logo it points at cannot ship independently.

How it was verified

  • vitest run tests/unit/protocol-ether-fi.test.ts: 22 passed
  • vitest run tests/unit/protocol-calldata.test.ts: 525 passed, golden matches without UPDATE_GOLDENS
  • vitest run tests/integration/protocol-ether-fi-onchain.test.ts: 12 passed against live mainnet. Every read decoded to its declared type, pool TVL and share count non-zero, getRate at or above 1e18, the three writes accepted by deployed bytecode
  • The share-price cross-check is the one worth naming: amountForShare(1e18), getEETHByWeETH(1e18) and getRate() are asserted byte-identical, so a wrong contract address fails even though each read on its own would return a plausible non-zero number
  • tsgo --noEmit: clean apart from the pre-existing @workflow/builders resolution miss in tests/unit/workflow-directive-detection.test.ts
  • biome check on every touched file: clean

Screenshots

Nothing renders beyond the plugin icon.


  • Targets staging
  • Title carries the issue number, or an exemption applies
  • pnpm check and pnpm type-check pass
  • No secrets, .env files, or credentials committed

AI assistance (Claude, Anthropic) was used in developing this change. The design, review and verification were done by the author. Verified locally before submitting: the unit suite (22 cases), the calldata goldens (525 cases), the on-chain integration suite (12 cases) against deployed mainnet bytecode, tsgo and biome.

Adds ether.fi as a first-class protocol: stake ETH for eETH via the
Liquidity Pool, wrap/unwrap between eETH and weETH, and read pool
accounting, exchange rates, and balances. Mainnet only; the withdrawal
queue and L2 weETH are deferred.

Three contracts, verified on mainnet before filing:
- liquidityPool 0x308861A430be4cce5502d0A12724771Fc6DaF216
- weeth         0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee
- eeth          0x35fA164735182de50811E8e2E824cFb9B6118ac2
weETH.eETH() returns the eETH address, and weETH.getRate() equals
LiquidityPool.amountForShare(1e18), so the set is coherent on-chain.

protocols/index.ts and lib/types/integration.ts are regenerated by
pnpm discover-plugins. Verified: 18-case unit test passes, tsgo and
biome clean, seed-workflow baseline unchanged.
Resolve the generated lib/types/integration.ts by taking upstream then
re-running discover-plugins.
… surface

Name every ABI output so the testData field expectations resolve through
structureAbiOutputs, and key the registry overrides to those names rather
than "result".

Fixes a silent fixture bug found while doing so: the testData bound
amount-for-share, shares-for-amount, get-weeth-by-eeth and
get-eeth-by-weeth by their raw ABI parameter names, but the overrides
rename those inputs. An unmatched binding key is dropped and the encoder
falls back to a type-derived default, so each 1e18 fixture was silently
encoding 1 wei. The goldens now carry 0de0b6b3a7640000 as intended, and a
unit case pins every binding key to a declared input name.

Adds what a new protocol ships beyond the definition:
- docs/plugins/ether-fi.md with the "Why no testnet entry" section, its
  _meta.ts key and an overview.md row
- tests/integration/protocol-ether-fi-onchain.test.ts, 12 cases: every
  read decoded against deployed bytecode, the three writes simulated, plus
  a cross-check that amountForShare, getEETHByWeETH and getRate agree
- tests/unit/__goldens__/protocol-calldata/ether-fi.json
- public/protocols/ether-fi.png from the Optimism token list
- chain-exclusion, EIP-55 and expectation-field unit cases
@github-actions

Copy link
Copy Markdown
Contributor

About the build check on this pull request

This pull request comes from a fork, so GitHub does not pass it the credentials build normally uses for our image registry cache and staging build configuration. The build still runs and still compiles the image, so a red build here is real; it just takes longer than on team branches.

Every workflow run on a pull request from a fork also waits for a maintainer to approve it, so checks can sit at "awaiting approval" for a while after each push. Nothing is needed from you for either of these.

@zkasuran

Copy link
Copy Markdown
Author

@suisuss, this is the ether.fi PR for #2391. Everything in your acceptance comment is in it:

  • docs/plugins/ether-fi.md, its _meta.ts key and an overview.md row
  • tests/integration/protocol-ether-fi-onchain.test.ts, 12 cases, modelled on the frax-ether-v2 one
  • public/protocols/ether-fi.png, the weETH mark from the Optimism token list at 256x256
  • testData declared with the golden committed at tests/unit/__goldens__/protocol-calldata/ether-fi.json
  • wrap and unwrap left skipped with reasons, with lib/test-data/chain-test-data.ts untouched
  • no coverage runner, so no shard list to register
  • the verified values and the date recorded in a comment on the definition

Two things I found while building it that you did not ask for, both real:

The ABI outputs were all declared "name": "", which is the same trap you flagged elsewhere: structureAbiOutputs returns the bare scalar, so a field: expectation cannot resolve. All 13 are named now, with the overrides keyed to those names rather than result.

The second was silent. The overrides rename the raw ABI parameters for readability, but the testData still bound the old names. An unmatched binding key is dropped rather than rejected. The encoder then falls back to a type-derived default, so amount-for-share, shares-for-amount, get-weeth-by-eeth and get-eeth-by-weeth were each encoding 1 wei where the fixture said 1 ETH. Nothing errored. The goldens carry 0de0b6b3a7640000 now, plus there is a unit case pinning every binding key to a declared input name so it cannot regress quietly.

On verification, the cross-check is the one worth a look: amountForShare(1e18), getEETHByWeETH(1e18) and getRate() are asserted byte-identical against live mainnet, so a wrong contract address fails even though each read alone would return a plausible non-zero number.

22 unit, 525 calldata goldens, 12 on-chain, tsgo and biome clean. Rebased on current staging. Ready for the CI approve-run when you have a moment.

On your question about the referral-taking deposit variant: I stayed on the no-argument deposit() you confirmed payable. Happy to switch if ether.fi would rather integrators used the referral entrypoint, though I would want to read the implementation ABI properly before claiming which one that is.

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.

feat(protocols): add ether.fi liquid restaking (stake, wrap/unwrap, reads)

1 participant