
add-vault-protocol
Add support for a new ERC-4626 vault protocol. Use when the user wants to integrate a new vault prot
Add vault protocol
This skill guides you through adding support for a new ERC-4626 vault protocol to the eth_defi library.
Tokenised fund protocols
Tokenised fund protocols belong under
eth_defi/tokenised_fund/{protocol_slug}/, rather than
eth_defi/erc_4626/vault_protocol/. Use
eth_defi/tokenised_fund/asseto/ and
eth_defi/tokenised_fund/securitize/ as the reference integrations. These
protocols commonly expose permissioned ERC-20 token shares and bespoke
subscription and redemption flows instead of ERC-4626 vault contracts.
Required inputs
Before starting, gather the following information from the user:
- Vault smart contract address - The address of an example vault contract on a blockchain
- Protocol name - Human-readable name (e.g., "Plutus", "IPOR", "Morpho")
- Protocol slug - Snake_case identifier for code (e.g., "plutus", "ipor", "morpho")
- Chain - Which blockchain (Ethereum, Arbitrum, Base, etc.)
- Block explorer URL - To fetch the ABI (e.g., Etherscan, Arbiscan, Basescan)
- Single vault protocol: Some protocols, especially ones issuing out their own stablecoin, are know to have only a single vault for the stablecoin staking. Example protocols are like like Spark, Ethena, Cap. In this case use
HARDCODED_PROTOCOLSclassification later, as there is no point to create complex vault smart contract detection patterns if the protocol does not need it. - Risk level: Optional. If not given, set to
None
Completion requirements
A new vault protocol integration is not complete unless it includes:
- Protocol detection or hardcoded address classification
- Vault class and
create_vault_instance()wiring - Deposit manager and public deposit/redemption flow capability, backed by a guarded fork transaction test
- Risk and fee matrix entries
- Protocol metadata YAML under
eth_defi/data/vaults/metadata/ - Original and post-processed protocol logos
- A post-processed
light.png: a light-coloured logo that remains legible on the frontend's dark backgrounds - Vault documentation and API documentation entries
- Focused tests for the new protocol
- A generated protocol-specific historical lead migration script that preserves unrelated vault database, reader-state and Parquet entries
Step-by-step implementation
Step 1: Download and store the ABI
- Fetch the vault smart contract ABI from the blockchain explorer
- Important: If the contract is a proxy, you need the implementation ABI, not the proxy ABI
- Check if the contract has a
implementation()function or similar - Use the explorer's "Read as Proxy" feature to get the implementation address
- Download the implementation contract's ABI
- Check if the contract has a
- Create the ABI directory and file:
eth_defi/abi/{protocol_slug}/ eth_defi/abi/{protocol_slug}/{ContractName}.json - Use
eth_defi/abi/lagoon/as a reference for structure
For a narrowly scoped adapter that only needs stable, no-argument view methods, using their canonical four-byte selectors is acceptable instead of storing a generated ABI. Link the authoritative ABI in the module docstring and add a fork regression test for every decoded value and scale.
Step 2: Create the vault class
Create eth_defi/erc_4626/vault_protocol/{protocol_slug}/vault.py following the patterns in:
eth_defi/erc_4626/vault_protocol/plutus/vault.py- Simple vault with hardcoded feeseth_defi/erc_4626/vault_protocol/ipor/vault.py- Complex vault with custom fee reading and multicall support
The vault class should:
"""Module docstring describing the protocol."""
import datetime
import logging
from eth_typing import BlockIdentifier
from eth_defi.erc_4626.vault import ERC4626Vault
logger = logging.getLogger(__name__)
class {ProtocolName}Vault(ERC4626Vault):
"""Protocol vault support.
One line description of the protocol.
- Add links to protocol documentation
- Add links to example contracts on block explorers
- Add links to github
- If fee information is documented or available as Github source code, link into it
"""
def get_management_fee(self, block_identifier: BlockIdentifier) -> float:
return None
def get_performance_fee(self, block_identifier: BlockIdentifier) -> float | None:
return None
def get_estimated_lock_up(self) -> datetime.timedelta | None:
return None
def get_link(self, referral: str | None = None) -> str:
return f"https://protocol-url.com/vault/{self.vault_address}"
For get_link() check the protocol website to find a direct link URL pattern to its vault. Usual formats:
- By address
- By chain id and address - for example Ethereum chain id is 1
- By chain name and address - use
get_chain_name(chain_id).lower()or simiar - Can be special for protocols just with one vault, it can be a single link with no pattern
- If you fail to figure this out, just link to the protocol homepage
Step 3: Add protocol feature enum
Edit eth_defi/erc_4626/core.py and add a new enum member to ERC4626Feature:
#: {Protocol Name}
#:
#: {Protocol URL}
{protocol_slug}_like = "{protocol_slug}_like"
Also update get_vault_protocol_name() to return the protocol name:
elif ERC4626Feature.{protocol_slug}_like in features:
return "{Protocol Name}"
Step 4: Add protocol identification probes
Edit eth_defi/erc_4626/classification.py:
Probe budget: Classification runs every probe against every candidate vault. Prefer one no-argument, protocol-specific view accessor per protocol. Use a second probe only when independently necessary contract variants cannot be safely identified by the first one, and document why both are required. Do not add fee, version, or other adapter data accessors merely to corroborate a classification; read those only after the adapter has been selected. Never add more than two protocol probes without explicit maintainer approval.
- In
create_probe_calls(), add a probe call that uniquely identifies this protocol:- Analyse the ABI and the vault implementation smart contract source code to find a function unique to this protocol
- Look for functions like
getProtocolSpecificData(), custom role constants, etc. and compare them to what is already implemented increate_probe_calls() - Make sure this call does not conflict with already configured protocols
- You can also use blockchain explorer's Contract > Read contract or Contract Read contract as proxy to figure out good ABI calls to detect this particular type of smart contracts
- If the protocol is a single vault protocol, use
HARDCODED_PROTOCOLSin classification.py instead
If you cannot find a such accessor function in the ABI or vault smart contract source, interrupt the skill and ask for user intervention.
# {Protocol Name}
# {Block explorer link}
{protocol_slug}_call = EncodedCall.from_keccak_signature(
address=address,
signature=Web3.keccak(text="uniqueFunction()")[0:4],
function="uniqueFunction",
data=b"",
extra_data=None,
)
yield {protocol_slug}_call
- In
identify_vault_features(), add detection logic:
if calls["uniqueFunction"].success:
features.add(ERC4626Feature.{protocol_slug}_like)
Step 5: Update create_vault_instance()
In eth_defi/erc_4626/classification.py, add a case for the new protocol in create_vault_instance():
elif ERC4626Feature.{protocol_slug}_like in features:
from eth_defi.erc_4626.vault_protocol.{protocol_slug}.vault import {ProtocolName}Vault
return {ProtocolName}Vault(web3, spec, token_cache=token_cache, features=features)
Step 6: Certify deposit and redemption flows
Every vault adapter must explicitly declare whether it supports deposits and redemptions. Do not treat ERC-4626 interface detection alone as permission to advertise deposit-manager support: public support requires a complete tested lifecycle.
-
Determine the flow from the vault contract and protocol documentation:
- Synchronous: the user approves the denomination token and directly calls
ERC-4626
deposit()/mint()andwithdraw()/redeem(). - Asynchronous: the vault uses a request, queue, epoch, settlement, claim, cooldown, or redemption-delay flow. Implement a protocol-specific deposit manager instead of certifying the generic manager.
- Unsupported: do not expose a partial manager. Leave the public capability
as
Noneuntil both directions are implemented and tested.
- Synchronous: the user approves the denomination token and directly calls
ERC-4626
-
For a standard synchronous ERC-4626 adapter, certify the inherited
ERC4626DepositManagerby adding the exact fully-qualified class name toCERTIFIED_SYNCHRONOUS_DEPOSIT_MANAGER_CLASSESineth_defi/erc_4626/vault.py:"eth_defi.erc_4626.vault_protocol.{protocol_slug}.vault.{ProtocolName}Vault",The inherited
get_deposit_manager()then returnsERC4626DepositManager, andget_deposit_manager_capability()exports the public fields:{ "can_deposit": True, "can_redeem": True, "deposit_flow": "synchronous", "redemption_flow": "synchronous", } -
Add a guarded Anvil fork test that uses an unlocked token holder to transfer the denomination token to an Anvil account, approves the vault, deposits through
vault.get_deposit_manager(), and redeems the exact minted share balance. Assert that the manager isERC4626DepositManager, both flow methods are synchronous, the public capability fields match the schema above, and the final share balance is zero. -
Add or update a no-RPC unit test for the exact-class allowlist. This prevents a future refactor from silently removing the advertised capability when RPC-backed tests are skipped.
Reference implementations:
- Generic manager and capability implementation:
eth_defi/erc_4626/deposit_redeem.pyandeth_defi/erc_4626/vault.py - Full synchronous approval/deposit/redeem fork flow:
tests/erc_4626/test_4626_deposit_redeem.py - Protocol-specific certified generic manager example:
tests/erc_4626/vault_protocol/test_kiln.py - No-RPC allowlist capability test:
tests/erc_4626/test_deposit_probe.py - Custom or non-generic manager examples:
eth_defi/erc_4626/vault_protocol/gains/andeth_defi/erc_4626/vault_protocol/upshift/vault.py
Step 7: Update risk and fee information
Update eth_defi/vault/risk.py with the protocol stub.
Set the initial risk level for the protocol in VAULT_PROTOCOL_RISK_MATRIX.
USe None if not given and this will be later updated by human judgement.
Update eth_defi/vault/fee.py with the protocol stub.
Set VAULT_PROTOCOL_FEE_MATRIX to None for newly added protocol.
Match get_vault_protocol_name() for the protocol name spelling.
Step 8: Add protocol metadata YAML
Create eth_defi/data/vaults/metadata/{protocol-slug}.yaml.
- Use the slug with dashes for metadata filenames if the protocol slug contains multiple words
- Include name, slug, short description, long description, fee description, links, and example smart contracts
- Use
eth_defi/data/vaults/README.mdas the schema reference - Include
trading_strategyandintegration_documentationlinks even if the Trading Strategy listing is not live yet - Write descriptions for a general audience. Explain what the product offers, who can use it, material eligibility or liquidity constraints, and how fees affect holders in plain language.
- Do not include library implementation, smart-contract interface, or data-pipeline
details. In particular, avoid standards and function names such as ERC-4626,
gem(),navprice(), and WAD scaling, as well as adapter, scanner, and TVL calculation internals. Put those details in the technical integration and API documentation instead.
Validate that the metadata can be parsed:
poetry run python - <<'PY'
from pathlib import Path
from eth_defi.vault.protocol_metadata import build_metadata_json
print(build_metadata_json(Path("eth_defi/data/vaults/metadata/{protocol-slug}.yaml"), "https://example.invalid")["name"])
PY
Step 9: Extract and post-process protocol logos
Protocol logos are required for vault protocol metadata and frontend listings. Do not skip this step unless no official or defensible logo source can be found after following the logo extraction workflow; if skipped, document why in the final response and in the logo README.
- Use the repo-local
extract-vault-protocol-logoskill.- Read
.claude/skills/extract-vault-protocol-logo/SKILL.md - Use the homepage from
eth_defi/data/vaults/metadata/{protocol-slug}.yaml - Save original logos under
eth_defi/data/vaults/original_logos/{protocol-slug}/ - Add a
README.mdin the original logo folder documenting sources and choices
- Read
- Use the repo-local
post-process-logoskill.- Read
.claude/skills/post-process-logo/SKILL.md - Create post-processed 256x256 PNG logos under
eth_defi/data/vaults/formatted_logos/{protocol-slug}/ - Always produce
light.png, a light-coloured logo for dark frontend backgrounds. This is the required listing-logo variant. - Produce
dark.pngwhen the source supports a distinct dark-coloured variant for light backgrounds; otherwise document why it is unavailable.
- Read
- Verify the metadata exporter sees the logos:
poetry run python - <<'PY'
from pathlib import Path
from eth_defi.vault.protocol_metadata import build_metadata_json
metadata = build_metadata_json(Path("eth_defi/data/vaults/metadata/{protocol-slug}.yaml"), "https://example.invalid")
print(metadata["logos"])
PY
Step 10: Create test file
New Anvil mainnet-fork characterisation tests must use the shared Anvil fork
- fixed fork-block + warm RPC-cache pattern (shared
anvil_fork_poolfixture, chain*_MIDNIGHT_BLOCKconstant,xdist_groupmarker). Do not launch a per-filefork_network_anvilatlatestor an ad-hoc block — that is non-reproducible, unshareable and defeats the CI RPC cache. The canonical, authoritative description of the pattern (rationale + how-to + copy-paste skeleton) lives in the module docstring ofeth_defi/testing/anvil_fork_pool.py— read it before writing the test.
Create tests/erc_4626/vault_protocol/test_{protocol_slug}.py following the
reference tests tests/erc_4626/vault_protocol/test_goat.py and
tests/erc_4626/vault_protocol/test_aarna.py (both read-only pooled forks):
"""Test {Protocol Name} vault metadata"""
import os
from pathlib import Path
import pytest
from web3 import Web3
import flaky
from eth_defi.erc_4626.classification import create_vault_instance_autodetect
from eth_defi.erc_4626.core import ERC4626Feature
from eth_defi.erc_4626.vault_protocol.{protocol_slug}.vault import {ProtocolName}Vault
from eth_defi.testing.anvil_fork_pool import AnvilForkPool
from eth_defi.testing.fork_blocks import {CHAIN}_MIDNIGHT_BLOCK
from eth_defi.vault.base import VaultTechnicalRisk
JSON_RPC_{CHAIN} = os.environ.get("JSON_RPC_{CHAIN}")
pytestmark = [
pytest.mark.skipif(JSON_RPC_{CHAIN} is None, reason="JSON_RPC_{CHAIN} needed to run these tests"),
# Co-locate every same-block {chain} sharer on one xdist worker so they
# reuse a single Anvil process under --dist loadgroup.
pytest.mark.xdist_group("fork:{chain}:midnight"),
]
@pytest.fixture(scope="module")
def web3(anvil_fork_pool: AnvilForkPool) -> Web3:
"""Web3 backed by a shared {chain} fork from the session-scoped pool.
Read-only test: shares one Anvil fork, so no snapshot/revert reset is
needed between tests.
"""
return anvil_fork_pool.get_web3(JSON_RPC_{CHAIN}, {CHAIN}_MIDNIGHT_BLOCK)
@flaky.flaky
def test_{protocol_slug}(
web3: Web3,
tmp_path: Path,
):
"""Read {Protocol Name} vault metadata"""
vault = create_vault_instance_autodetect(
web3,
vault_address="{vault_address}",
)
assert isinstance(vault, {ProtocolName}Vault)
assert vault.get_protocol_name() == "{Protocol Name}"
# Add assertation about vault feature flags here, like:
# assert vault.features == {ERC4626Feature.goat_like}
# Add assertions for fee data we know
# assert vault.get_management_fee("latest") == ...
# assert vault.get_performance_fee("latest") == ...
# Add assertion for the protcol risk level
# assert vault.get_risk() == VaultTechnicalRisk.unknown
- Update the test file for the correct blockchain, using that chain's
*_MIDNIGHT_BLOCKconstant frometh_defi/testing/fork_blocks.py. If the chain has no constant yet, add one (see thefork_blocks.pymodule docstring) or, for a chain without archive history (e.g. Monad), fall back to a state-relative assertion perCLAUDE.md. - Validate that the example vault actually has state at that block before normalising onto it; if it does not, give the test its own fixed block.
- When you run the test and if the user does not have JSON-RPC configured for this chain, interrupt the skill and tell user to update his test environment variables.
After adding it, run the test module and fix any issues.
Step 11: Add module init.py
Create eth_defi/erc_4626/vault_protocol/{protocol_slug}/__init__.py:
"""{Protocol Name} protocol integration."""
Step 12: Update documentation
- Add protocol to
docs/source/vaults - Add protocol to
docs/source/vaults/index.rst - Add API stub under
docs/source/api/{protocol_slug}/index.rst - Cross-reference the API stub in
docs/source/api/index.rst - Include protocol name
- Search web for a short description, two paragraph
- Add a link to the protocol home page and documentation
- Search Github/web for a Github repo link of the smart contracts
- Search protocol homepage for Twitter link and add it to the documentation
- Search protocol homepage and documentation audits page
- Search protocol homepage and documentation fees page
- Check if DefiLLama has a page for this protocol
- Add the new modules to the protocol index page TOC
- Add the protocol to the master index in
docs/source/vaults/index.rst
Examples include
docs/source/vaults/plutus/index.rst,docs/source/vaults/truefi/index.rst,docs/source/api/vaults/index.rst,
Step 13: Run all vault protocol detection tests
Check that all ERC-4626 tests pass after adding a new vault protocol by running all testse in tests/erc_4626/vault_protocol folder.
Run all vault testes:
source .local-test.env && poetry run pytest -n auto -k vault_protocol
Fix any issues if found.
Step 14: Format the codebase
Format the newly added files with poetry run ruff format.
Step 15: Add feed protocol YAML entry
Create a feed YAML file at eth_defi/data/feeds/protocols/{protocol-slug}.yaml so the protocol's social media posts are collected by the feed scanner. For full schema documentation and collection behaviour details, see eth_defi/feed/README-feed.md.
Use the protocol slug with dashes (not underscores). E.g. lagoon-finance, ipor-fusion, goat-protocol.
The file should follow this format:
feeder-id: {protocol-slug}
name: {Protocol Name}
role: protocol
website: {homepage URL}
twitter: {twitter handle without @}
linkedin: {linkedin company slug}
rss: {RSS or Atom feed URL}
To fill the fields:
- feeder-id: Same slug as the filename (without
.yaml) - name: Human-readable protocol name, matching
get_vault_protocol_name()output - role: Always
protocolfor vault protocols - website: Protocol homepage URL (already gathered in earlier steps)
- twitter: Twitter/X handle without
@— find on the protocol homepage. If no Twitter found, omit the field. - linkedin: LinkedIn company page slug (the part after
linkedin.com/company/). Find via web search for"{protocol name}" site:linkedin.com/company. If not found, omit the field. - rss: Look for an RSS/Atom feed URL. Common patterns:
- Medium blogs:
https://medium.com/feed/@{handle}orhttps://medium.com/feed/{publication} - Substack:
https://{name}.substack.com/feed - Blog pages: Check for
<link rel="alternate" type="application/rss+xml">in page source - If no RSS feed exists, add a comment:
# rss: not found — {reason}
- Medium blogs:
Example (simple):
feeder-id: plutus
name: Plutus
role: protocol
website: https://plutus.fi/
twitter: plutus_fi_x
rss: https://medium.com/feed/@plutus.fi
Example (no RSS):
feeder-id: lagoon-finance
name: Lagoon Finance
role: protocol
website: https://lagoon.finance/
twitter: lagoon_finance
linkedin: lagoon-finance
# rss: not found — blog is at lagoon.finance/blog but has no RSS feed
Step 16: Verification checklist
After implementation, verify:
- ABI file is correctly placed in
eth_defi/abi/{protocol_slug}/, or the protocol is intentionally usingHARDCODED_PROTOCOLS - Vault class inherits from
ERC4626Vault -
ERC4626Featureenum has the new protocol -
get_vault_protocol_name()returns the correct name -
create_probe_calls()has a unique probe for the protocol, or the protocol is intentionally usingHARDCODED_PROTOCOLS -
identify_vault_features()orHARDCODED_PROTOCOLScorrectly identifies the protocol -
create_vault_instance()creates the correct vault class - Test file runs successfully with:
source .local-test.env && poetry run pytest tests/erc_4626/vault_protocol/test_{protocol_slug}.py -v - Metadata YAML parses successfully
- Original logos are saved under
eth_defi/data/vaults/original_logos/{protocol-slug}/ - Post-processed logos are saved under
eth_defi/data/vaults/formatted_logos/{protocol-slug}/ -
formatted_logos/{protocol-slug}/light.pngexists and is a light-coloured logo suitable for dark backgrounds - Metadata exporter sees the post-processed logos
- API documents have been updated
- Check that homepage link in the API documentation takes to the correct homepage
- Check that Twitter link in the API documentation works and takes to the same Twitter account as listed on the protocol homepage
- Feed YAML file exists at
eth_defi/data/feeds/protocols/{protocol-slug}.yaml
If there are problems with the checklist, ask for human assistance.
Step 17: Changelog
- Update changelog line in
CHANGELOG.mdand add a note of added new protocol
Step 18: Pull request (optional)
After everything is done, open a pull request, but only if the user asks you to.
gh pr create \
--title "Add new vault protocol: {protocol name}" \
--body $'Protocol: {protocok name}\nHomepage: {homepage link}\nGithub: {github link}\nDocs: {docs link}\nExample contract: {blockchain explorer link}" \
--base master
Finding unique protocol identifiers
To find a function that uniquely identifies the protocol:
-
Read the ABI and look for:
- Protocol-specific role constants (e.g.,
SAY_TRADER_ROLE()for Plutus) - Custom getter functions (e.g.,
getPerformanceFeeData()for IPOR) - Protocol registry calls (e.g.,
MORPHO()for Morpho) - Unique configuration functions
- Protocol-specific role constants (e.g.,
-
Verify the function is truly unique by checking it doesn't exist in other protocols
-
Some protocols may need name-based detection if no unique function exists:
name = calls["name"].result if name: name = name.decode("utf-8", errors="ignore") if "ProtocolName" in name: features.add(ERC4626Feature.{protocol_slug}_like)
Example ABI structure
The ABI JSON file should contain the contract's ABI array. Example:
{
"abi": [
{
"inputs": [],
"name": "totalAssets",
"outputs": [{ "type": "uint256" }],
"stateMutability": "view",
"type": "function"
}
]
}
Or just the array directly:
[
{
"inputs": [],
"name": "totalAssets",
"outputs": [{ "type": "uint256" }],
"stateMutability": "view",
"type": "function"
}
]