This page is an internal review and hardening pass (2026-07-13) by the project's own developers plus automated tooling. It is not an independent audit, formal verification, or a safety guarantee. Vibing Farmer runs on Stellar testnet and Base Sepolia. It is not cleared for unrestricted mainnet value.
First-party contracts and the client/relayer code that enforces their auth model:
- Soroban (Rust, SDK 26.1.0):
funding_router,agent_account,autofarm_vault,blend_strategy,registry,attestation - Solidity (0.8.23, OpenZeppelin 5.x):
YieldRouter,AaveV3Adapter4626 - Off-chain: ZeroDev session-key policy (
frontend/src/base/policyEngine.js,frontend/src/wallet/mandate.js), CCTP relayer (relayer/), fee-bump allowlist (frontend/api/stellar-relay)
Blend, Soroswap, Circle CCTP, Aave, ZeroDev, OpenZeppelin, and the Stellar host are dependency trust boundaries. We use them with validation and least privilege. We did not re-audit them.
What this hardening assumes an attacker can do:
- Control ordinary user accounts
- Steal an ephemeral Stellar or ZeroDev session key before it expires
- Supply callback-capable ERC-20 / ERC-4626 contracts if an admin allowlists them by mistake
- Return malformed, reverting, negative, or inflated values from a vault strategy
- Watch the mempool and front-run predictable deployment salts
- Take down any single keeper, relayer, or RPC
Each control below has regression tests that fail if it breaks.
- One-signature grant, native leash.
funding_router.grantnests SEP-41token.approve(owner→router, budget, expiry_ledger)under the owner's single auth entry. Token allowance is budget and expiry. The router holds no funds and has no admin or upgrade path. Only factory-deployed agents canpull, and only from their recorded owner. Fake-agent attacks are blocked at the structure (tested with the real agent wasm). - Grant validation, fail-closed. Empty agent lists, dead allowance ledgers, zero period durations, past scope expiries, and non-positive caps/budgets are rejected before approval or deploy.
- Owner-bound deployment salt. Agent addresses derive from
sha256("vibing-farmer/agent-salt/v1" ‖ router ‖ owner ‖ raw salt). Another owner cannot squat a predictable salt namespace. - Session keys are deposit-only.
agent_account.__check_authchecks the ed25519 session signature and the constructor-pinned scope: onlydeposit@vault(cap-per-period, rolling window, expiry) andpull@deploying-routerare allowed. Approve, transfer, and everything else fails closed. - Per-agent revoke on-chain.
agent_account.revoke()(owner-signed, idempotent) sets therevokedflag that__check_authchecks, zeroes the agent's vault allowance, and emitsagent_revoked.owner_withdrawalso clears the allowance on exit. The frontend kill switch talks to the agent contract directly; it does not need the relayer. - Registry is metadata only.
registry.authorize(agent)fills stored fields from the agent's ownscope_of()under the derived owner's auth. Records cannot change owner.is_active/is_revokedfail closed for unknown agents. Nothing uses the Registry for authorization decisions.
- Inflation guard: dead shares + minimum first deposit; pro-rata share pricing with zero/negative-NAV guards on deposit and redeem.
- Strategies are untrusted. NAV clamps negative reports and saturates inflated sums. Compound slices are precomputed; a failed slice (trap, wrong-type return, dishonest pull) stays idle instead of being rerouted. Transient allowances clear after every attempt. Rebalance and emergency de-risk trust only the observed token balance delta, and fail closed (
StrategyMisbehaved) when a strategy report disagrees. - Fail-closed parameters.
max_move_bpscapped at 10_000. Cooldown math uses checked arithmetic (overflow means still cooling down, never a trap). - Incident hatches.
quarantine_strategy(strategy, acknowledged_loss)removes a bricked strategy without calling it, restores deposit/redeem/price liveness, and emits the acknowledged write-off.emergency_derisksets its flag before any external call, drains best-effort, and is idempotent. Redeem drains strategies best-effort and never traps on a single bricked strategy. - Upgrade timelock (2026-07-19). The instant admin
upgradeis removed. Upgrades are two-phase:schedule_upgrade(wasm_hash)announces on-chain (event + publicpending_upgrade()view; the keeper radar and the UI banner surface it),execute_upgrade()only succeeds after a compile-time 3-day delay (TIMELOCK_DELAY_S = 259_200, aconst— no setter, so no set-zero-then-upgrade bypass), andcancel_upgrade()aborts a bad or hijacked schedule. Same-transaction schedule+execute is impossible (eta is strictly in the future); re-scheduling always resets the full delay; the pending record is cleared before the wasm swap, so an unknown-hash trap reverts the whole call atomically.redeemis not pause-gated and keeps working during the window (regression-tested), so holders can always exit before a swap lands. New storage keys and error codes are append-only, keeping the in-place upgrade of the live vault storage-compatible.
- Live NAV, not book value.
balance()reads the pool'sget_positions/get_reserveand prices bTokens asfloor(b_tokens · b_rate / 1e12)with overflow-safe (I256-backed) checked math. Accrued yield raises NAV; socialized bad debt lowers it. Malformed reserve data (negative values, unrepresentable products) returnsInvalidReserveDatainstead of trapping. - Finite crisis exits. Full drains size a finite request from the live position.
i128::MAXnever goes to Blend (bToken conversion overflows on that whenb_rate < 1e12, which is exactly a bad-debt crisis case). - No stranded yield. Harvest uses the live position map, not book principal, so residual yield after a principal-sized withdrawal still realizes. Shortfalls mark the book down.
- Reentrancy-guarded value flow. OpenZeppelin
ReentrancyGuardon all public mutation entrypoints of both contracts; immutable canonical asset; pool allowlisting with exact-asset checks; exact pre/post balance-delta enforcement; transient allowances reset to zero; disabled pools stay withdrawable so incident response cannot trap users. The unenforceable address-based performance fee was removed, not patched. - Session-key policy parity. ZeroDev permissions grant exactly two calls: USDC
approvewith spender pinned to the deployed router and amount capped by the mandate, andYieldRouter.depositcapped the same way. Tests decode the relayer's batched calls and require each to pass the generated policy, so drift fails CI. No policy grants an unconstrained approval spender.
soroban: cargo +1.91.0 test --workspace --all-targets → 134 tests pass
cargo +1.91.0 clippy --workspace --all-targets -- -D warnings
stellar contract build (stellar-cli 27.0.0, rustc 1.92.0) → 6 wasms
base-contracts: forge test (unit + fuzz + invariant + adversarial reentrancy) → 51 pass
forge build --sizes
slither . --exclude-dependencies → 17 results, all triaged (below)
frontend: npm test (vitest) and npm run build
relayer: npm test (vitest)
2026-07-19 (upgrade-timelock pass): cargo +1.91.0 test --workspace --all-targets → 151 tests pass (adds schedule/execute/cancel gates, eta-overflow fail-closed, reschedule-resets-delay, redeem-during-pending); clippy -D warnings clean; stellar contract build → new autofarm_vault wasm a073c7af75db169ec115b4af8af63bc7b7b88c58b90be348ac343a0432bb78a0.
Slither triage. All 17 remaining findings are accepted:
reentrancy-*andincorrect-equalityhit the deliberate pre/post balance-deltarequires inside functions already behindnonReentrant. Adversarial callback tests inYieldRouterReentrancy.t.solandAaveV3Adapter.t.solshow reentry reverts. Strict equality is the fail-closed exactness check; incompatible tokens should revert.pragmaandnaming-conventionare informational (OZ^0.8.20vs first-party^0.8.23; Aave's canonicalUNDERLYING_ASSET_ADDRESScasing).
- Admin keys. Vault admin can register strategies, set the keeper, pause deposits, and schedule/execute/cancel wasm upgrades — but can no longer swap bytecode instantly: every upgrade is announced on-chain three days ahead,
redeemcannot be paused, and a hijacked schedule can be cancelled. The admin role was transferred on 2026-07-20 to a dedicated 2-of-3 native Stellar multisig account (vf-vault-admin—transfer_admin_roletx86e1b8c8…8861, 2-of-3accept_admin_transfertx65d9d089…9f91), so a single compromised admin key is not sufficient, and even full admin compromise cannot silently drain the vault.vf-deployerno longer holds the vault admin role. Funding router has no admin. Deployed agent accounts are immutable. - Dependency protocols. Blend insolvency, a bad Soroswap quote, a Circle CCTP outage, or an Aave freeze can degrade or trap that leg. Controls limit blast radius (finite requests, slippage floors, balance-delta checks). They cannot make a broken dependency solvent.
- Session-key compromise. Blast radius is that agent's scope: deposits into the pinned vault within cap/expiry, plus pulls within the owner's remaining allowance. Funds only move owner → agent → vault and back toward the owner. Response: per-agent
revoke()and/or globalapprove(router, 0). Both need one user signature. Neither needs the relayer. - Keeper / relayer failure. Fail-safe, not fail-deadly. Deposits, redeems, and both kill switches work without them. Only compounding, rebalancing, and the lifeboat need the keeper (and a live user mandate).
- Partial lifeboat drain.
emergency_deriskis idempotent: once the derisked flag is set, a retry returnsOk(0)without re-running the drain. If a strategy withdrawal reverts mid-rescue (e.g. frozen pool) and the user mandate expires before recovery, no keeper path re-drains that strategy. Recovery still possible: adminemergency_withdraw(strategy)(not derisk-gated), ordinary userredeem(best-effort drain), or while the mandate is live aresume()+emergency_derisk()pair. Accepted as a low-severity operational edge. - Very long agent scopes. An agent's vault allowance is sized to outlast its scope but is capped at the network's max entry TTL (~1 year at 5s ledgers). A scope longer than that can brick deposits once the allowance lapses. Funds stay recoverable via
owner_withdrawand a fresh grant. Keep scope expiries within the max TTL. - Unit-test blind spots. Soroban tests hit contract logic against mocks, not the live Blend testnet pool. Fork tests cover Aave on Base. Live smoke on testnet is part of the rollout gate below.
Stellar stack redeployed from this hardening pass on 2026-07-14 (deployer: vf-deployer). Addresses in deployments/stellar-testnet.json, README, and PRD point at the hardened contracts. Per-contract wasm hashes and deploy/smoke receipts are in the deployments JSON (hardenedRedeploy key and per-contract notes).
Rollout gate, in order:
- ✅ Uploaded
agent_accountwasm v3 → deployedfunding_routerpinning that hash - ✅ Deployed
blend_strategy+ freshautofarm_vault(pre-hardening vault retired; not upgraded in place); strategy registered; keeper + mandate authority wired - ✅ Deployed
registry(newauthorize(agent)derived-record ABI) - ⏳ Base leg (optional cross-chain):
YieldRouter+ adapter/pools redeploy and ZeroDev policy regeneration still pending. Base Sepolia addresses indeployments/base-sepolia.jsonstill predate the hardening pass - ✅ Live smoke on the new stack: grant → pull → deposit → per-agent revoke → global revoke (allowance→0) → owner exit (
owner_withdraw) → compound (idle sweep into Blend) → derisk/resume under mandate. Base approve + deposit/withdraw smoke deferred with the Base leg - ✅
deployments/stellar-testnet.jsonupdated from confirmed receipts; legacy notices removed from README/PRD - ✅ Upgrade-timelock cutover complete 2026-07-20: timelock vault wasm uploaded to testnet (
a073c7af…bb78a0) ✅; dedicated 2-of-3 multisig admin account configured and proven on-chain (1-sig rejectedTxBadAuth, 2-sig accepted) ✅; in-place bootstrapupgradeto the timelock wasm ✅ (txf9103d9d…4de5— the vault's last instant upgrade);transfer_admin_roleto the multisig ✅ (tx86e1b8c8…8861, accepted 2-of-3 tx65d9d089…9f91); two-phase upgrade smoke ✅ all-PASS live (scripts/soroban/upgrade-timelock-smoke.sh— 1-sig schedule rejected, 2-sig committed, early execute blockedError(Contract, #25), cancel cleared)
Open a GitHub security advisory, or email the maintainer (see the repository profile). Do not open public issues for exploitable findings. Even on testnet, we fix before we disclose.