Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Action Families — Dev Context
Audience: anyone adding a new protocol adapter, reviewing adapter PRs, or wiring a new
product module on the backend.
TL;DR: every adapter now declares which action families it belongs to via Solidity
interface inheritance. Some interfaces enforce a strict signature (lending, cooldown staking),
others are empty markers (swap, LP) because real protocol surfaces diverge. Full rationale in
ADR 0001.
1. Why this exists
Before, every adapter just inherited
Initializableand shipped whatever methods its upstreamprotocol needed. That works but:
backend's
ADAPTER_SELECTORSto memorize per-adapter exceptions.BenqiLendAdapterhas no Solidity-level hint that it's aCompound-fork money market and that
MoonwellAdaptershould mirror its shape.Interfaces solve the second and third problem cheaply (zero runtime cost for markers, a
vtable entry or two for strict). They do not, on their own, solve the first — consistent
naming across adapters is still a PR-review concern.
2. The four families
Location:
contracts/interfaces/.ISwapAdapterILPAdapterILendAdaptersupply / redeem / borrow / repay / enterMarkets / exitMarketIStakeAdapterstake / requestUnlock / redeemMarker = categorization only. You declare
is ISwapAdapterto tag your adapter, but youpick whatever swap signature your protocol needs.
Strict = you must implement the exact signatures (or the compiler rejects the
contract), and you mark them with
override.When is something strict vs marker?
Rule of thumb: if the upstream protocol surface is standardized de facto across the
ecosystem, we lock it down. Compound has been the reference money-market ABI since 2019 — any
Compound fork (Benqi, Moonwell, Venus, Sonne) matches it 1:1. Lido's withdrawal queue has
become the reference cooldown-stake shape for ETH and AVAX liquid staking.
AMMs do not have this luxury — Uniswap V2, Aerodrome with its
bool stable, Curve withstable pools, Trader Joe V1 flat / V2 Liquidity Book — no common signature preserves all of
them without losing information.
3. How to add a new adapter to an existing family
3.1 Lending (strict
ILendAdapter)Example: wiring Moonwell on Base.
Forgetting
overrideon one of the six will fail compilation. That's the point.3.2 Stake (strict
IStakeAdapter)Cooldown-based liquid staking. Example signature template:
Per-user unlock tracking should live in the adapter storage — each user has their own
BeaconProxy, so a contract-level array is effectively per-user. See
SAVAXAdapter.solfor the canonicalpattern.
3.3 Swap (marker
ISwapAdapter)You're free to invent the signature, but please:
tokenIn(orpath[0]when using paths).amountIn,amountOutMin,recipient, in that order.tokenIn == address(0)as native asset (msg.value) where applicable.amountOut.the last bool/enum arg.
Then register the selector in
backend/src/shared/bundle-builder.tsusing the full Soliditysignature:
Different selectors per adapter is fine —
PanoramaExecutorV2dispatches blindly.3.4 LP (marker
ILPAdapter)Same spirit as swap: declare
is ILPAdapterto categorize, then modeladdLiquidity,removeLiquidity,stake,unstaketo match your protocol. SeeAerodromeAdapterV2for areference on refunds and gauge integration.
4. How to add a brand-new family
Only do this when a new protocol class appears that doesn't fit any existing family —
perpetuals, options, bridges, etc. Steps:
docs/adr/000N-<name>.md).contracts/interfaces/I<Family>Adapter.sol.Do not retrofit a strict interface onto pre-existing adapters without a version bump —
promoting a family from marker to strict is a signature constraint that can break
already-deployed adapters.
5. Deploy procedure after adapter changes
Adding
is IXxxAdapter+overridedoes not change runtime behavior — only the metadatahash at the tail of the bytecode changes. Still:
PanoramaExecutorV2does not redeploy.Rollout checklist:
All existing user proxies immediately delegate to the new impl. No user action required.
Storage-layout changes — the one thing to be careful about
If you add a new state variable to an adapter:
__gap[50]by the number of new slots you consumed.uint256 foo→uint256[49] private __gap;.If you reorder or remove a storage variable: stop, open a PR, get a review. This is the
only class of change that can brick existing user proxies on upgrade.
6. Testing expectations
After any adapter change:
Minimum coverage for a new adapter:
7. Backend integration checklist
When wiring a new adapter on the backend side:
registerProtocol()(id, chain, beacon address, selectors).ADAPTER_SELECTORSusing full Solidity signatures (not just names).BundleBuilder— never hand-constructPreparedTransaction.adapterDatawithethers.AbiCoder.defaultAbiCoder().encode(types, values)—types must match the adapter function's Solidity types exactly.
backend/src/modules/<protocol>/withusecases/,controllers/,routes/.8. FAQ
Q: I want to add a new lending adapter but the protocol's
supplyreturns nothing — Benqireturns a uint. Can I change the interface return type?
A: No, because that breaks every existing implementer. Either return 0 when you have no
useful value, or bring it up for an ADR amendment.
Q: My swap adapter needs 3 extra args (slippage curve, feeTier, recipient). Does it still
fit
ISwapAdapter?A: Yes. Marker interfaces impose no signature. Just be consistent about ordering and document
the encoding in the module's
usecases/.Q: Can one adapter implement two families?
A: Yes — see
AerodromeAdapterV2 is IProtocolAdapter, ISwapAdapter, ILPAdapter. Usejudgment: if the two surfaces are genuinely coupled (same router call site), keep them in one
adapter; if they're independent (e.g., lending + staking), split.
Q: Do I need
overrideon a marker interface?A: No — markers have no functions to override. You only write
overridefor methods declaredin a strict parent interface.