Skip to main content

yield-vault-patterns

Use when building ERC-4626 tokenized vaults, yield aggregators, or share-based deposit/withdraw systems. Covers share math, rounding, first depositor attack mitigation, and complete implementation patterns.

Zur Installation springen

Quellinformationen

Repository
ccashwell/evm-cortex
Letzte Quellaktivität
10. April 2026 um 16:31
Erkannte Sprache von SKILL.md
Englisch
Sterne
131
Forks
18

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
yield-vault-patterns
description
Use when building ERC-4626 tokenized vaults, yield aggregators, or share-based deposit/withdraw systems. Covers share math, rounding, first depositor attack mitigation, and complete implementation patterns.
# ERC-4626 Tokenized Vault Patterns ## Overview ERC-4626 standardizes yield-bearing vaults. Depositors receive shares proportional to their deposit. Shares appreciate as the vault earns yield. **Core Invariant**: `shares * totalAssets / totalShares = user's assets` ## Share Price Math ``` deposit: shares = assets * totalSupply / totalAssets (round DOWN) mint: assets = shares * totalAssets / totalSupply (round UP) withdraw: shares = assets * totalSupply / totalAssets (round UP) redeem: assets = shares * totalAssets / totalSupply (round DOWN) ``` **Rounding rule**: Always round in favor of the vault (against the user) to prevent value extraction. ## Complete ERC-4626 Implementation ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; contract YieldVault is ERC20, IERC4626 { using SafeERC20 for IERC20; using Math for uint256; IERC20 public immutable asset_; uint8 private immutable _decimals; // Virtual shares/assets to prevent first depositor attack uint256 internal constant VIRTUAL_SHARES = 1e3; uint256 internal constant VIRTUAL_ASSETS = 1; constructor(IERC20 _asset, string memory name, string memory symbol) ERC20(name, symbol) { asset_ = _asset; _decimals = ERC20(address(_asset)).decimals(); } function asset() external view returns (address) { return address(asset_); } function decimals() public view override returns (uint8) { return _decimals; } function totalAssets() public view returns (uint256) { return asset_.balanceOf(address(this)); } function convertToShares(uint256 assets) public view returns (uint256) { return assets.mulDiv( totalSupply() + VIRTUAL_SHARES, totalAssets() + VIRTUAL_ASSETS, Math.Rounding.Floor ); } function convertToAssets(uint256 shares) public view returns (uint256) { return shares.mulDiv( totalAssets() + VIRTUAL_ASSETS, totalSupply() + VIRTUAL_SHARES, Math.Rounding.Floor ); } function maxDeposit(address) external pure returns (uint256) { return type(uint256).max; } function maxMint(address) external pure returns (uint256) { return type(uint256).max; } function maxWithdraw(address owner) external view returns (uint256) { return convertToAssets(balanceOf(owner)); } function maxRedeem(address owner) external view returns (uint256) { return balanceOf(owner); } function previewDeposit(uint256 assets) public view returns (uint256) { return convertToShares(assets); } function previewMint(uint256 shares) public view returns (uint256) { return shares.mulDiv( totalAssets() + VIRTUAL_ASSETS, totalSupply() + VIRTUAL_SHARES, Math.Rounding.Ceil // round UP against user ); } function previewWithdraw(uint256 assets) public view returns (uint256) { return assets.mulDiv( totalSupply() + VIRTUAL_SHARES, totalAssets() + VIRTUAL_ASSETS, Math.Rounding.Ceil // round UP against user ); } function previewRedeem(uint256 shares) public view returns (uint256) { return convertToAssets(shares); } function deposit(uint256 assets, address receiver) external returns (uint256 shares) { shares = previewDeposit(assets); require(shares > 0, "zero shares"); asset_.safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); } function mint(uint256 shares, address receiver) external returns (uint256 assets) { assets = previewMint(shares); asset_.safeTransferFrom(msg.sender, address(this), assets); _mint(receiver, shares); emit Deposit(msg.sender, receiver, assets, shares); } function withdraw(uint256 assets, address receiver, address owner_) external returns (uint256 shares) { shares = previewWithdraw(assets); if (msg.sender != owner_) { _spendAllowance(owner_, msg.sender, shares); } _burn(owner_, shares); asset_.safeTransfer(receiver, assets); emit Withdraw(msg.sender, receiver, owner_, assets, shares); } function redeem(uint256 shares, address receiver, address owner_) external returns (uint256 assets) { assets = previewRedeem(shares); if (msg.sender != owner_) { _spendAllowance(owner_, msg.sender, shares); } _burn(owner_, shares); asset_.safeTransfer(receiver, assets); emit Withdraw(msg.sender, receiver, owner_, assets, shares); } } ``` ## First Depositor Attack Mitigation Without protection, an attacker can: 1. Deposit 1 wei to get 1 share 2. Donate a large amount directly to inflate `totalAssets` 3. Next depositor gets 0 shares due to rounding **Virtual shares/assets** solve this by ensuring the exchange rate always has a floor: ```solidity uint256 internal constant VIRTUAL_SHARES = 1e3; uint256 internal constant VIRTUAL_ASSETS = 1; // Share calculation uses (totalSupply + VIRTUAL_SHARES) / (totalAssets + VIRTUAL_ASSETS) // Cost of attack = VIRTUAL_SHARES * donation_amount / VIRTUAL_ASSETS ``` OpenZeppelin's ERC4626 uses `_decimalsOffset()` which is equivalent. ## Yield Strategy Integration ```solidity function totalAssets() public view override returns (uint256) { // Include assets deployed to yield strategies return asset_.balanceOf(address(this)) + _deployedAssets(); } function harvest() external { // Collect yield from strategy, increasing totalAssets // Share price automatically increases for all holders strategy.claim(); } ``` ## Checklist - [ ] Round DOWN for `deposit` and `redeem` (favors vault) - [ ] Round UP for `mint` and `withdraw` (favors vault) - [ ] Implement first depositor attack mitigation (virtual shares or minimum deposit) - [ ] `totalAssets()` includes all assets under management (not just balance) - [ ] `preview*` functions match actual execution (same rounding) - [ ] `max*` functions account for strategy liquidity limits - [ ] Handle ERC-20 tokens with non-standard decimals - [ ] Test share price manipulation via direct token transfers - [ ] Emit `Deposit` and `Withdraw` events per spec
Auf GitHub ansehen