Section 17 of 18
Vault: Emergency Shutdown, Governance, and Role Hierarchy
What You Are Building
The deployable Vault contract. This is the final link in the Vault chain (Vault is VaultFees is VaultWithdraw is VaultLockedProfit is VaultReport is VaultCreditDebt is VaultAddStrategy is VaultTotalAssets is VaultDeposit is VaultShareMath is VaultShareToken is VaultStorage). It adds:
- The actual constructor.
- The emergency shutdown switch.
- The 2-step governance transfer.
- Single-step setters for
management,guardian,depositLimit. - A
sweepfunction for stuck non-tokenassets.
After this section, you have a working Yearn V2 vault you could deploy to mainnet (with a concrete strategy from the Strategy chain).
The Constructor
constructor(
address token_,
string memory name_,
string memory symbol_,
address governance_,
address management_,
address guardian_,
address rewards_
) {
initVault(token_, name_, symbol_, governance_, management_, guardian_, rewards_);
}
Single constructor in the chain. Calls initVault from section 9 (which has the validation, sets activation = block.timestamp, sets default lockedProfitDegradation for ~6h unlock).
Sections 9-16 inherit VaultStorage but have no constructors of their own. The student tests sections in isolation by deploying the section's contract directly and calling initVault manually. In production, only this constructor is called once.
Role Hierarchy
| Role | Privileges | Transfer |
|---|---|---|
governance | All parameter changes, transfer of itself, sweep | 2-step (setGovernance + acceptGovernance) |
management | Day-to-day knob turning (debtRatios, queue, fees within caps) | Single-step by governance |
guardian | Can ENABLE emergency shutdown only | Single-step by governance |
strategist | Per-strategy controls (lives on the strategy contract) | , |
keeper | Calls harvest/tend on strategies (lives on the strategy contract) | , |
Strategist and keeper are NOT vault state, they live on each strategy contract (section 8). The vault has four roles, the strategy has two more.
Emergency Shutdown
function setEmergencyShutdown(bool active) external {
if (active) {
require(
msg.sender == governance || msg.sender == guardian,
"Vault/only-gov-or-guardian"
);
} else {
require(msg.sender == governance, "Vault/only-governance");
}
emergencyShutdown = active;
emit EmergencyShutdown(active);
}
Asymmetric access: governance OR guardian can ENABLE shutdown; only governance can DISABLE it. Guardian is meant to be a fast-trigger role (a security firm's monitoring service, a hot wallet), they should be able to pull the brake without being able to release it.
What changes when emergencyShutdown == true:
creditAvailable()returns 0 for every strategy. No new capital flows out.debtOutstanding()returns each strategy's fulltotalDebt. Next harvest forces every strategy to liquidate everything.deposit()reverts (require(!emergencyShutdown)from section 5).withdraw()is unaffected. Users can still get out.
The bug to avoid: ordering errors. If withdraw() checked emergency status before allowing the path through strategies, users would be stuck. The vault deliberately keeps withdraw enabled because emergency shutdown is supposed to PROTECT users, not trap them.
2-Step Governance Transfer
function setGovernance(address _governance) external onlyGovernance {
require(_governance != address(0), "Vault/zero-governance");
pendingGovernance = _governance;
}
function acceptGovernance() external {
require(msg.sender == pendingGovernance, "Vault/only-pending-gov");
governance = pendingGovernance;
pendingGovernance = address(0);
}
Step 1 stages the new address. Step 2 is the new address claiming the role. Both must transact, separated in time. Defends against fat-fingered transfer (typo, dead address, controllable-by-attacker address).
Compare to the OpenZeppelin Ownable2Step pattern, same idea. Yearn V2 had this in 2020, before it was the convention. Single-step setGovernance should never be in production code.
Other Setters
function setManagement(address _management) external onlyGovernance { ... }
function setGuardian(address _guardian) external onlyGovernance { ... }
function setDepositLimit(uint256 limit) external onlyGovernance { ... }
All single-step. The lower-stakes roles don't need the 2-step protection (governance can recover from a bad management address by setting a new one; the bad management address can't lock governance out).
Sweep
function sweep(address asset, uint256 amount) external onlyGovernance {
require(asset != address(token), "Vault/cannot-sweep-token");
require(IERC20(asset).transfer(governance, amount), "Vault/sweep-failed");
}
Cleanup function. Anyone can send a random ERC-20 to the vault address (an airdrop, an error). sweep lets governance recover those tokens to its own address.
The critical require: asset != address(token). Without that, governance could sweep the underlying token, draining depositor funds. A "sweep" without this check is a backdoor.
Yearn V2 has a more elaborate version with a per-strategy protectedTokens list (a strategy may legitimately hold non-want tokens that shouldn't be sweepable, e.g. a Curve LP holding CRV before selling). Out of scope here; the simple version is fine for the core teaching.
What's Next
Section 18 is the final-build capstone. The complete Vault + StrategyHarvest system. Test verifies the whole chain integrates, deploys, and runs an end-to-end harvest cycle.