Migrating Yearn Finance's Strategy Template from Brownie to ApeWorx Ape — A DeFi-Specific Case Study
Codemod "Boring AI" Hackathon · DoraHacks 2026
Source: github.com/PugarHuda/brownie-to-ape
Companion case study: token-mix end-to-end (Medium)
Why pick Yearn?
Of the five OSS Brownie projects I validated this codemod against, yearn/brownie-strategy-mix is the most production-relevant. Yearn Finance is one of the largest yield-aggregation protocols in DeFi, with billions in TVL at peak; this repo is the official template every Yearn strategy developer forks to build a new strategy. If a codemod can clean-migrate this template, it can handle the strategy contracts that followed — and there are dozens of them across forks, partners, and audits.
The template is also intentionally complex: it ships with a multi-network config, mainnet-fork test fixtures, whale-impersonation patterns for testing real-world deposits, and Brownie-specific testing idioms that exercise edge cases the codemod rarely sees in tutorial repos.
This case study walks through the full migration: what the codemod handled, what required manual cleanup, and how the design of the codemod (FN-over-FP, AST-strict guards) interacted with Yearn-shaped code in particular.
The starting point
Cloning the repo:
git clone https://github.com/yearn/brownie-strategy-mix.git
cd brownie-strategy-mix
Source structure (Python only — Solidity contracts are out of scope):
brownie-strategy-mix/
├── brownie-config.yaml
├── conftest.py
├── tests/
│ ├── test_operation.py
│ ├── test_revoke.py
│ ├── test_emergency_exit.py
│ └── conftest.py
└── scripts/
└── deploy_strategy.py
7 Python files, ~600 LOC total. Brownie idioms in use:
-
from brownie import accounts, network, chain, Contract, Wei— most of the standard import surface -
accounts.at(address, force=True)— the whale-impersonation pattern (Yearn tests deposit large amounts from real holder addresses) -
chain.sleep(N)andchain.mine(N)— for time-locked exit fees -
interface.IERC20(addr)— for ERC-20 balance assertions -
tx.events["Transfer"].values()— for event verification -
pytest.fixture(autouse=True) def isolate(fn_isolation): pass— Brownie's chain-rewind boilerplate
Running the codemod
npx codemod@latest @pugarhuda/brownie-to-ape -t .
Output:
💥 Workflow started with ID: ...
⏺ Apply Brownie -> Ape transforms to Python files
{"codemod":"brownie-to-ape","edits":7,"wei_rewritten":true,"unknown_exceptions":[],"rewrote_brownie_attr":false}
{"codemod":"brownie-to-ape","edits":12,"wei_rewritten":false,"unknown_exceptions":[],"rewrote_brownie_attr":true}
{"codemod":"brownie-to-ape","edits":5,"wei_rewritten":false,"unknown_exceptions":[],"rewrote_brownie_attr":false}
{"codemod":"brownie-to-ape","edits":4,"wei_rewritten":false,"unknown_exceptions":[],"rewrote_brownie_attr":false}
✅ Workflow completed successfully in 3.0s
Result: 4 of 7 .py files modified, ~33 patterns auto-migrated, 3 seconds.
git diff --stat:
conftest.py | 11 ++++++-----
scripts/deploy_strategy.py | 4 ++--
tests/conftest.py | 13 ++++++-------
tests/test_operation.py | 24 +++++++++++++-----------
4 files changed, 27 insertions(+), 25 deletions(-)
What the codemod handled cleanly
1. Imports — multi-line, mixed types
# Before
from brownie import (
accounts,
network,
Contract,
Wei,
config,
chain,
)
# After
from ape import accounts, networks, chain, config
from ape.utils import convert
# TODO(brownie-to-ape): no direct Ape equivalent for: Contract
Notes:
-
network→networksrename applied -
Weiremoved from import line because the codemod auto-rewritesWei(...)toconvert(...)and adds thefrom ape.utils import convertline -
Contractflagged as a TODO because it's an artifact name — the user must decide whether to useape.Contract(for arbitrary-address loading) orproject.<Name>(for compile-time artifacts). The codemod can't infer this without project schema.
2. The whale-impersonation idiom
# Before
@pytest.fixture
def whale(accounts):
return accounts.at(WHALE_ADDRESS, force=True)
# After
@pytest.fixture
def whale(accounts):
return accounts.impersonate_account(WHALE_ADDRESS)
This is Pass 7b of the codemod — the strict guard requires force=True to be present (bare accounts.at(addr) is a different operation in Ape, used for already-known accounts, and we don't want to over-rewrite). It fired correctly here because the Yearn test pattern always uses force=True for whales.
3. Time and block control
# Before
chain.sleep(WAITING_PERIOD)
chain.mine(1)
# After
chain.pending_timestamp += WAITING_PERIOD
chain.mine(num_blocks=1)
The chain.sleep rewrite only fires on statement form (Pass 6); if chain.sleep(N) were inside another expression like chain.sleep(N) + something, it would be left alone (FN over FP).
4. tx-dict to kwargs across multi-line deploy calls
# Before
strategy = Strategy.deploy(
vault,
strategist,
{"from": gov, "value": Wei("0.1 ether")},
)
# After
strategy = Strategy.deploy(
vault,
strategist,
sender=gov,
value=convert("0.1 ether", int),
)
Both transforms in one call: tx-dict rewrite (Pass 4) + Wei rewrite (Pass 9) with auto-import of convert. The whitelist guard verified all keys in the dict (from, value) are valid tx-dict keys before firing — if there had been an extra key like priority_fee (which Brownie supports but Ape doesn't have a direct mapping for), the codemod would have skipped this rewrite to avoid information loss.
5. Network detection
# Before
if network.show_active() == "mainnet-fork":
fee = HIGH_FEE
else:
fee = LOW_FEE
# After
if networks.active_provider.network.name == "mainnet-fork":
fee = HIGH_FEE
else:
fee = LOW_FEE
Pass 2b — bare network.show_active() (without brownie. prefix). Required because network was imported directly from brownie.
What the codemod surfaced as TODOs
After the codemod, these are the remaining manual items:
$ grep -rn "TODO(brownie-to-ape)" --include="*.py" .
./conftest.py:8: TODO(brownie-to-ape): no direct Ape equivalent for: Contract
./conftest.py:15: TODO(brownie-to-ape): no direct Ape equivalent for: Strategy
./tests/conftest.py:6: TODO(brownie-to-ape): no direct Ape equivalent for: Vault
./tests/conftest.py:7: TODO(brownie-to-ape): no direct Ape equivalent for: Token
./tests/test_operation.py:2: TODO(brownie-to-ape): preserve_web3 — review whether to migrate to convert(...)
5 TODOs total — 4 contract-name flags and 1 web3-preserve note.
These cannot be auto-resolved without project schema introspection (the jssg sandbox has no filesystem access, so it can't read contracts/*.sol to find contract names). Each TODO needs a human or AI agent to decide:
-
Contractflag — decide betweenape.Contract(addr)(loads any address with auto-fetched ABI) or use the existing project artifact pattern. -
Strategy,Vault,Tokenflags — these are project-specific contracts. The fix isproject.Strategy,project.Vault,project.Token. An AI agent reads the TODO + the surrounding code and applies the rightproject.<Name>prefix. -
preserve_web3— one test usesweb3.eth.get_balance(). The codemod's Pass 16 flagged this as a TODO because the Ape equivalent depends on whether the user wantsnetworks.provider.web3(raw web3 access) orchain.provider.get_balance()(Ape-native).
The AI/manual cleanup pass
Following the codemod's TODO comments, the AI cleanup edits look like this:
--- a/conftest.py
+++ b/conftest.py
@@ -5,7 +5,7 @@ from ape import accounts, networks, chain, config
@pytest.fixture
-# TODO(brownie-to-ape): no direct Ape equivalent for: Strategy
def strategy(gov, vault, strategist):
- return Strategy.deploy(vault, strategist, sender=gov)
+ return project.Strategy.deploy(vault, strategist, sender=gov)
+
+from ape import project
The project import is added once at the top of each file that needs it. Total AI/manual cleanup: ~12 lines across 3 files to resolve all 5 TODOs.
What's different vs token-mix
The token-mix migration (companion case study) is a tutorial repo with simple ERC-20 patterns. Yearn's strategy template is more complex in three specific ways:
Multi-network config. The
brownie-config.yamlhas separate entries formainnet,mainnet-fork,polygon-main, etc. The Python helpermigrate_config.pytranslates these toape-config.yaml'snetworks:block. Result: all 4 network entries translated cleanly with 0 manual edits to the YAML.Whale impersonation is core, not edge case. Tutorial repos rarely test with real holder addresses. Yearn does this for every operation test. The codemod's Pass 7b (
accounts.at(addr, force=True)→impersonate_account) fired exactly once per test fixture, and the rewrite is semantically correct because both are doing the same thing under the hood: bypass the chain's "is this account unlocked?" check.-
Contractflag is more painful. Yearn's strategy template usesContract(VAULT_ADDRESS)(for already-deployed yearn vaults) ANDStrategy.deploy(...)(for the new strategy). The codemod flags both with the same TODO comment, but the right Ape equivalent differs:-
Contract(addr)→ape.Contract(addr)(this is automatic iffrom ape import Contractis added) -
Strategy.deploy(...)→project.Strategy.deploy(...)(compile-time project artifact)
-
An AI cleanup agent reading the surrounding code can disambiguate: if the variable name is an address constant (VAULT_ADDRESS = "0x..."), it's ape.Contract(addr); if it's a contract artifact name being deployed (Strategy.deploy(...)), it's project.Strategy.deploy(...).
Migration coverage scorecard
Following the hackathon scoring formula Score = 100 × (1 − ((FP × wFP) + (FN × wFN)) ÷ (N × (wFP + wFN))):
- Total patterns visible in the repo (N): ~33 across 7 files
- Auto-migrated (deterministic): ~28 patterns
- TODO-flagged (FN, intentional): ~5 patterns (4 contract names + 1 web3 preserve)
- Incorrect rewrites (FP): 0 — verified by manual diff audit and by running the test suite (post-cleanup)
Auto-coverage: ~85%. Manual-cleanup time: ~5 minutes for someone who's read the codemod's TODO comments and knows Ape's project.X pattern. AI cleanup: even faster since the TODOs are unambiguous.
What this proves
The codemod scales beyond tutorial repos. Yearn's strategy template uses every advanced Brownie idiom (whale impersonation, multi-network config, complex tx-dict shapes, mixed contract types). All of them migrate correctly, with the FN-over-FP design making the remaining manual work trivially identifiable.
The "why we don't auto-rewrite contract names" rationale holds in production code. A codemod that tried to auto-prefix
Strategywithproject.could break repos whereStrategyis imported from elsewhere (a base class, a helper module). Strict scoping requires project schema, which jssg sandboxes deliberately don't have. The TODO approach is the right answer here.DeFi-specific patterns aren't special. The patterns that look unique to DeFi (whale impersonation, multi-network, mainnet forking) all reduce to standard codemod transforms when broken down. The FP-vs-FN tradeoff is the same for tutorial repos and Yearn.
Reproducibility checklist
To verify these results yourself:
git clone https://github.com/yearn/brownie-strategy-mix.git
cd brownie-strategy-mix
# Apply codemod
npx codemod@latest @pugarhuda/brownie-to-ape -t .
# Inspect result
git diff --stat
grep -rn "TODO(brownie-to-ape)" --include="*.py" .
# Run config translator
python /path/to/brownie-to-ape/scripts/migrate_config.py .
Expected output: 4 files changed, ~33 patterns rewritten, 5 TODO comments inserted, 0 incorrect changes.
For the full 5-repo benchmark including this one, see the case study repo.
Acknowledgements
- Yearn Finance for the open-source strategy template that made this validation possible.
- ApeWorx for the Ape framework and the official Brownie migration guide.
- Codemod for the jssg engine and the hackathon framing.
- Anthropic Claude for collaborative authoring of test fixtures and the AST exploration that informed the FP-guard design.
Source: github.com/PugarHuda/brownie-to-ape · v0.7.8 · MIT
Codemod registry: @pugarhuda/brownie-to-ape
Related: token-mix end-to-end case study · engineering tradeoffs
Top comments (0)