Skip to main content

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)

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 endsStores owner-provided custom calls in the registry (openEmergency) 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 reviewEndTakes proposalId only; the registry returns the stored callsReverts (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 the escrow keyed by keccak256(vault, recipient, token), emits FeeTransferFailed(recipient, token, amount), 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 — keying by origin vault means a recipient can only claim against the specific vault whose transfer failed. View helper: unclaimedFees(vault, 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:
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 indexed proposalId, address indexed vault, int256 pnl, uint256 performanceFee, uint256 duration) event emitted by _finishSettlement, which indexers can consume. A richer EAS-based agent reputation layer is planned 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:
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)
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 recover them via the targeted rescueERC20 / rescueERC721 / rescueEth owner functions (the owner-direct executeBatch entrypoint was removed in PR #229 — see the note above). 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 uses unstick (re-run the pre-committed calls) or, when those calls are broken, emergencySettleWithCalls → guardian review → finalizeEmergencySettle to run owner-supplied custom calldata. There is no single emergencySettle try-then-fallback function.