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:- Anyone (permissionless) calls
executeProposal(proposalId)on the governor (no arguments beyond the ID) - Governor verifies: proposal is Approved (guardian review passed), within execution window, no other strategy live, cooldown elapsed
- Governor records the proposal ID in
_activeProposal[vault]— this is whatvault.redemptionsLocked()reads - Governor snapshots vault’s deposit asset balance (
capitalSnapshot) - Governor calls
vault.executeGovernorBatch(proposal.executeCalls)— vault runs the execution calls via its governor-gated entrypoint - All DeFi positions (mTokens, LP tokens, borrows) now live on the vault address
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.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:- Execution deadline — time to start executing after approval (
executionWindow, governor-controlled) - Strategy duration — time the position runs before settlement (
strategyDuration, agent-proposed, capped bymaxStrategyDuration)
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:| Function | Who | When | Calls | Notes |
|---|---|---|---|---|
settleProposal | Proposer anytime; anyone after duration | Proposer: anytime after execution. Others: after strategyDuration ends | Pre-committed settlementCalls from proposal | The happy path; zero trust |
unstick | Vault owner | After strategyDuration ends | Pre-committed settlementCalls only — no custom calldata, no fallback | Force-triggers a settlement that the proposer is not finalizing |
emergencySettleWithCalls | Vault owner | After strategyDuration ends | Commits the hash of owner-provided custom calls and opens a guardian review window | Owner’s bond must cover requiredOwnerBond(vault) at call time; reverts otherwise |
cancelEmergencySettle | Vault owner | Before reviewEnd | — | Self-recall of an emergencySettleWithCalls the owner wants to retract (e.g. wrong calldata) |
finalizeEmergencySettle | Vault owner | After reviewEnd | Re-submits the exact calls whose hash was committed | Reverts (and slashes owner bond) if guardian block quorum reached; otherwise executes via vault.executeGovernorBatch and transitions to Settled |
- Standard Settlement
- unstick (owner-instant, pre-committed calls)
- emergencySettleWithCalls (guardian-reviewed custom calls)
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
strategyDurationexpires — no trust required
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:settleProposal— happy path. Zero trust required; anyone can call after duration.unstick— pre-committed calls are still correct, just need a push. No new calldata → no bond required, no guardian review.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.cancelEmergencySettle— safety valve for an honest owner who submitted the wrong calldata and wants to retract before the window closes.
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
executeProposalreverts - Purpose: gives depositors an exit window between strategies — if they don’t like the next approved proposal, they can leave
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: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:
executeProposal(proposalId)— runsexecuteCalls(the opening portion)settleProposal(proposalId)— runssettlementCalls(the closing portion)
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.