Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.sherwood.sh/llms.txt

Use this file to discover all available pages before exploring further.

Mandate Execution

When a proposal is approved, the pre-committed calls are executed directly by the vault:
  1. Anyone (permissionless) calls executeProposal(proposalId) on the governor (no arguments beyond the ID)
  2. Governor verifies: proposal is Approved (guardian review passed), within execution window, no other strategy live, cooldown elapsed
  3. Governor records the proposal ID in _activeProposal[vault] — this is what vault.redemptionsLocked() reads
  4. Governor snapshots vault’s deposit asset balance (capitalSnapshot)
  5. Governor calls vault.executeGovernorBatch(proposal.executeCalls) — vault runs the execution calls via its governor-gated entrypoint
  6. All DeFi positions (mTokens, LP tokens, borrows) now live on the vault address
No new input from the agent at execution time. The calls were locked in at proposal creation and voted on by shareholders. Execution is just replaying what was approved.
Pull-model redemption lock — no lockRedemptions() function. There is no separate state-flipping call on the vault. Instead, the vault exposes redemptionsLocked() which reads governor.getActiveProposal(address(this)) != 0 live every time. Setting _activeProposal[vault] inside executeProposal is what flips the lock; clearing it inside _finishSettlement is what unlocks it. This removes a whole class of state-desync bugs where the vault could disagree with the governor about whether a strategy is live.
Redemption lock: When a strategy is live (Executed state) the vault’s redemptionsLocked() returns true, so withdraw / redeem / deposit / rescueERC20 all revert. Depositors who want to exit early can sell their shares on the WOOD/SHARES liquidity pool (see Economics).
executeBatch on the vault is gone. The owner-direct executeBatch entrypoint on the vault was removed in commit f616ec4 (PR #229 predecessor) to close a privilege-escalation bug where a compromised owner could run arbitrary batches while a strategy was live (bypassing redemptionsLocked). All strategy execution now flows through vault.executeGovernorBatch, which is governor-only. Stranded assets leave the vault via the targeted rescueERC20 / rescueERC721 / rescueEth owner functions, which themselves check redemptionsLocked() before firing.

Strategy Duration & Settlement

Two separate clocks govern the lifecycle:
  1. Execution deadline — time to start executing after approval (executionWindow, governor-controlled)
  2. Strategy duration — time the position runs before settlement (strategyDuration, agent-proposed, capped by maxStrategyDuration)
|-- voting --|-- exec window --|------ strategy duration ------|-- cooldown --|
   propose      execute calls      position is live     settlement    withdrawals open
                                                                      (no new strategies)

Settlement Paths

Since the exact on-chain state at settlement time cannot be predicted (slippage, pool state, interest accrued), pre-committed unwind calls may revert. Sherwood provides the standard path plus a four-function emergency split introduced in PR #229:
FunctionWhoWhenCallsNotes
settleProposalProposer anytime; anyone after durationProposer: anytime after execution. Others: after strategyDuration endsPre-committed settlementCalls from proposalThe happy path; zero trust
unstickVault ownerAfter strategyDuration endsPre-committed settlementCalls only — no custom calldata, no fallbackForce-triggers a settlement that the proposer is not finalizing
emergencySettleWithCallsVault ownerAfter strategyDuration endsCommits the hash of owner-provided custom calls and opens a guardian review windowOwner’s bond must cover requiredOwnerBond(vault) at call time; reverts otherwise
cancelEmergencySettleVault ownerBefore reviewEndSelf-recall of an emergencySettleWithCalls the owner wants to retract (e.g. wrong calldata)
finalizeEmergencySettleVault ownerAfter reviewEndRe-submits the exact calls whose hash was committedReverts (and slashes owner bond) if guardian block quorum reached; otherwise executes via vault.executeGovernorBatch and transitions to Settled

settleProposal

The standard settlement path uses the pre-committed settlementCalls that shareholders voted on.Who can call it:
  • The proposer (agent) can call at any time after execution — they have the most context about when to close
  • Anyone (keeper, depositor, bot) can call after strategyDuration expires — no trust required
This is the happy path. The pre-committed unwind calls close all positions and return capital to the vault. P&L is calculated and fees are distributed.Risk: Pre-committed calls may revert due to stale parameters (slippage, exact repayment amounts). If this happens, the owner uses unstick or emergencySettleWithCalls as the fallback.

Why this split?

Pre-committed unwind calls are a best-effort prediction of future on-chain state. Slippage, interest accrual, pool rebalancing, and oracle updates can all cause them to revert. The split covers every failure mode without giving the owner an unbounded escape hatch:
  1. settleProposal — happy path. Zero trust required; anyone can call after duration.
  2. unstick — pre-committed calls are still correct, just need a push. No new calldata → no bond required, no guardian review.
  3. emergencySettleWithCalls → review → finalize — custom calldata requires a bonded owner and survives a 24h guardian review. An abusive owner gets slashed; an honest owner gets their funds back plus a successful settlement.
  4. cancelEmergencySettle — safety valve for an honest owner who submitted the wrong calldata and wants to retract before the window closes.
Together these replace the single emergencySettle(proposalId, calls) function from pre-PR-#229. The old function combined “execute arbitrary calldata if pre-committed calls revert” into a single owner-gated call — unbounded trust in a compromised owner was the primary escalation vector.
Fee transfers are wrapped in try/catch, with a pull-claim escrow for blacklisted recipients. Inside _distributeFees, every vault.transferPerformanceFee(...) call (protocol fee, agent / co-proposers, management fee) is wrapped in try/catch. On any failure — including USDC blacklisting the recipient — the governor credits the owed amount to _unclaimedFees[recipient][token], emits FeeTransferFailed(recipient, token, amount, reason), and continues. settleProposal therefore never reverts on a single bad recipient. Once the failure condition is cleared (recipient rotated, unblocked, etc.), the recipient calls claimUnclaimedFees(vault, token) to pull their escrowed balance. View helper: unclaimedFees(recipient, token). This closes item W-1 in the pre-mainnet punch list.

Cooldown Window

After settlement, a cooldown period begins before any new strategy can execute on that vault.
  • Duration: cooldownPeriod (governor parameter, owner-controlled)
  • During cooldown: redemptions are re-enabled, depositors can withdraw
  • During cooldown: proposals can still be submitted and voted on, but executeProposal reverts
  • Purpose: gives depositors an exit window between strategies — if they don’t like the next approved proposal, they can leave
Safety bounds: cooldownPeriod: min 1 hour, max 30 days

P&L Calculation

Since only one strategy runs per vault at a time, P&L is calculated via a simple balance snapshot:
Execute:
  1. Governor snapshots vault's deposit asset balance → capitalSnapshot
  2. Vault executes the pre-approved executeCalls via executeGovernorBatch
     (positions now live on the vault address)

During strategy:
  - Position is live on the vault (e.g. mTokens, LP tokens, borrowed assets)
  - Agent cannot interact with vault directly — only governor can trigger calls
  - Redemptions are locked (via redemptionsLocked() pull-check against governor)

Settle:
  Path 1 — Standard settle (pre-committed calls):
    1. Proposer calls anytime, or anyone calls after strategyDuration
    2. Vault executes the pre-committed settlementCalls via executeGovernorBatch
    3. P&L = vault.depositAssetBalance() - capitalSnapshot
    4. If P&L > 0: fees distributed (protocol → agent → management). If P&L <= 0: no fee.

  Path 2 — Owner unstick (pre-committed calls, owner-forced):
    1. Owner calls unstick(proposalId) after strategyDuration
    2. Same pre-committed settlementCalls, same P&L math
    3. No bond requirement, no guardian review

  Path 3 — Emergency settle with custom calls (owner-bonded, guardian-reviewed):
    1. Owner calls emergencySettleWithCalls(proposalId, calls) — bond rechecked, calls hash committed
    2. 24h guardian review window; guardians can block; owner can cancel
    3. Owner calls finalizeEmergencySettle(proposalId, calls) after reviewEnd
    4. If guardians blocked: owner bond slashed, function reverts
    5. Otherwise: vault executes calls via executeGovernorBatch, same P&L math

  All paths end with:
    - _activeProposal[vault] cleared (redemptions unlock automatically)
    - Cooldown starts
    - Proposal state → Settled
No PnL EAS attestation in V1. Earlier designs proposed a STRATEGY_PNL EAS schema minted by the governor at settlement to create an immutable on-chain track record. That feature is not implemented in the current codebase and is deferred. The on-chain record today is the ProposalSettled(uint256 proposalId, int256 pnl, uint256 totalFee) event emitted by _finishSettlement, which indexers can consume. A richer EAS-based agent reputation layer is planned for V1.5 alongside the full guardian reward + reputation track.

Full Lifecycle in calls[]

The proposal commits the complete strategy lifecycle in two separate call arrays — opening calls (executeCalls) and closing calls (settlementCalls). The agent commits everything upfront:
Example for a Moonwell borrow + Uniswap swap strategy:

executeCalls[] (opening — run at execution):
  1. approve WETH to Moonwell
  2. supply WETH as collateral
  3. borrow USDC
  4. approve USDC to Uniswap
  5. swap USDC → target token

settlementCalls[] (closing — run at settlement):
  6. swap target token → USDC
  7. repay USDC borrow
  8. redeem WETH collateral
  9. swap WETH → USDC (if needed) — convert everything back to deposit asset
Shareholders vote on the entire sequence. They can inspect every step — open and close. Execution and settlement use their respective call arrays:
  1. executeProposal(proposalId) — runs executeCalls (the opening portion)
  2. settleProposal(proposalId) — runs settlementCalls (the closing portion)
struct StrategyProposal {
    ...
    BatchExecutorLib.Call[] executeCalls;     // opening calls
    BatchExecutorLib.Call[] settlementCalls;  // closing calls
    ...
}
Settlement should return to deposit asset. After the unwind calls execute, the vault should hold the deposit asset (e.g. USDC) again. If non-deposit-asset tokens remain on the vault after settlement, the owner can manually handle them via executeBatch (owner-only). Stale parameters: Since pre-committed unwind calls are a prediction of future state, agents should use generous slippage tolerances. If standard settlement reverts, the vault owner can use emergencySettle as a backstop — it tries the pre-committed calls first via try/catch, then falls back to the owner’s custom calls.