Skip to main content

erc4626-patterns

Use when building tokenized vaults with ERC-4626. Covers deposit/mint/withdraw/redeem, share math, rounding rules, first depositor attack mitigation, yield strategies, and integration patterns.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
ccashwell/evm-cortex
آخر نشاط في المصدر
١٠ أبريل ٢٠٢٦ في ١٦:٣١
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٣١
التفرعات
١٨

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
erc4626-patterns
description
Use when building tokenized vaults with ERC-4626. Covers deposit/mint/withdraw/redeem, share math, rounding rules, first depositor attack mitigation, yield strategies, and integration patterns.
# ERC-4626 Tokenized Vault Patterns ## Core Interface ERC-4626 standardizes yield-bearing vaults. Users deposit assets and receive shares: ```solidity interface IERC4626 is IERC20 { function asset() external view returns (address); function totalAssets() external view returns (uint256); // Deposit flow: user specifies assets function deposit(uint256 assets, address receiver) external returns (uint256 shares); function mint(uint256 shares, address receiver) external returns (uint256 assets); // Withdraw flow: user specifies assets or shares function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); // Preview functions (simulate without executing) function previewDeposit(uint256 assets) external view returns (uint256 shares); function previewMint(uint256 shares) external view returns (uint256 assets); function previewWithdraw(uint256 assets) external view returns (uint256 shares); function previewRedeem(uint256 shares) external view returns (uint256 assets); // Limits function maxDeposit(address receiver) external view returns (uint256); function maxMint(address receiver) external view returns (uint256); function maxWithdraw(address owner) external view returns (uint256); function maxRedeem(address owner) external view returns (uint256); function convertToShares(uint256 assets) external view returns (uint256); function convertToAssets(uint256 shares) external view returns (uint256); } ``` ## Secure Implementation with Virtual Shares ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/Math.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; contract SecureVault is ERC4626 { using SafeERC20 for IERC20; constructor(IERC20 asset_) ERC4626(asset_) ERC20("Vault Shares", "vASSET") {} /// @dev OpenZeppelin ERC4626 already uses virtual shares/assets /// (decimalsOffset = 0 by default, but _decimalsOffset() can be overridden). /// Override to add +3 offset for first-depositor attack mitigation: function _decimalsOffset() internal pure override returns (uint8) { return 3; } } ``` ## Share Math and Rounding Rules The fundamental equation: `shares * totalAssets == assets * totalSupply` ``` shares = assets * totalSupply / totalAssets (deposit, round DOWN → favor vault) assets = shares * totalAssets / totalSupply (redeem, round DOWN → favor vault) ``` **Rounding convention** (always favor the vault to prevent rounding exploits): | Operation | User sends | User receives | Round | |-----------|-----------|---------------|-------| | deposit | assets | shares | DOWN (fewer shares) | | mint | assets | shares | UP (more assets) | | withdraw | shares | assets | UP (more shares burned) | | redeem | shares | assets | DOWN (fewer assets) | ```solidity // OpenZeppelin handles rounding internally: function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); } ``` ## First Depositor Attack Mitigation The attack: first depositor mints 1 share, then donates large amount of assets directly to the vault. Subsequent depositors get 0 shares due to rounding. **Solution 1: Virtual shares/assets (recommended)** OpenZeppelin adds virtual offset to prevent manipulation: ```solidity function _decimalsOffset() internal pure override returns (uint8) { return 3; // 1000 virtual shares — cost of attack becomes prohibitive } ``` **Solution 2: Dead shares on first deposit** ```solidity function _afterDeposit(uint256 assets, uint256 shares) internal virtual { if (totalSupply() == shares) { // First deposit: burn minimum shares to address(1) uint256 deadShares = 10 ** decimals() / 1000; _mint(address(1), deadShares); } } ``` ## Yield Strategy Integration ```solidity contract YieldVault is ERC4626 { ILendingPool public immutable lendingPool; constructor(IERC20 asset_, ILendingPool pool_) ERC4626(asset_) ERC20("Yield Vault", "yASSET") { lendingPool = pool_; asset_.approve(address(pool_), type(uint256).max); } function totalAssets() public view override returns (uint256) { return lendingPool.balanceOf(address(this)); } function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal override { super._deposit(caller, receiver, assets, shares); IERC20(asset()).safeTransfer(address(lendingPool), assets); lendingPool.deposit(assets); } function _withdraw( address caller, address receiver, address owner, uint256 assets, uint256 shares ) internal override { lendingPool.withdraw(assets); super._withdraw(caller, receiver, owner, assets, shares); } } ``` ## Max Functions and Access Control ```solidity // Pausable vault example function maxDeposit(address) public view override returns (uint256) { return paused() ? 0 : type(uint256).max; } function maxMint(address) public view override returns (uint256) { return paused() ? 0 : type(uint256).max; } // Deposit caps uint256 public depositCap; function maxDeposit(address) public view override returns (uint256) { uint256 remaining = depositCap > totalAssets() ? depositCap - totalAssets() : 0; return paused() ? 0 : remaining; } ``` ## Testing ERC-4626 ```solidity function test_depositAndRedeem_roundTrip() public { uint256 assets = 1e18; deal(address(token), alice, assets); vm.startPrank(alice); token.approve(address(vault), assets); uint256 shares = vault.deposit(assets, alice); assertEq(vault.balanceOf(alice), shares); assertEq(vault.convertToAssets(shares), assets); uint256 redeemed = vault.redeem(shares, alice, alice); assertEq(redeemed, assets); vm.stopPrank(); } function test_previewMatchesActual() public { uint256 assets = 1e18; uint256 previewShares = vault.previewDeposit(assets); deal(address(token), alice, assets); vm.startPrank(alice); token.approve(address(vault), assets); uint256 actualShares = vault.deposit(assets, alice); vm.stopPrank(); assertEq(actualShares, previewShares, "Preview must match actual"); } ``` ## Security Checklist - [ ] Use virtual shares (`_decimalsOffset >= 3`) to prevent first depositor attack - [ ] Rounding always favors the vault (never the user) - [ ] Preview functions are accurate (match actual operations) - [ ] `totalAssets` cannot be manipulated via direct token transfers - [ ] Max functions return 0 when vault is paused or at capacity - [ ] Handle fee-on-transfer tokens if supported - [ ] Reentrancy guards on deposit/withdraw (especially with external protocol calls) - [ ] Test with zero deposits, dust amounts, and very large amounts
عرض على GitHub