# Scratch से Minimal ERC-4626 Vault बनाएं
ERC-4626 tokenized vaults का standard है। Aave, Yearn, Compound — सभी इसे adopt कर रहे हैं। आइए scratch से एक vault build करें।
ERC-4626 क्या है?
Tokenized Vault Standard — users assets deposit करते हैं, shares receive करते हैं।
User deposits 100 USDC
→ Vault mints 100 shares
→ Vault USDC invest करता है
→ Yield generates होता है
→ User redeems shares
→ Gets 100 USDC + yield
Core Concepts
1. Assets vs Shares
- Assets: Underlying token (USDC, WETH)
- Shares: Vault tokens (vUSDC, vWETH)
2. Exchange Rate
sharesमूल्य = totalAssets / totalShares
Initial: 1 share = 1 asset
After yield: 1 share = 1.1 assets (10% yield)
Interface
ERC-4626 extends ERC-20:
interface IERC4626 is IERC20 {
// Metadata
function asset() external view returns (address);
function totalAssets() external view returns (uint);
// Deposit
function deposit(uint assets, address receiver) external returns (uint shares);
function mint(uint shares, address receiver) external returns (uint assets);
// Withdraw
function withdraw(uint assets, address receiver, address owner) external returns (uint shares);
function redeem(uint shares, address receiver, address owner) external returns (uint assets);
// Conversion
function convertToShares(uint assets) external view returns (uint);
function convertToAssets(uint shares) external view returns (uint);
// Limits
function maxDeposit(address) external view returns (uint);
function maxMint(address) external view returns (uint);
function maxWithdraw(address owner) external view returns (uint);
function maxRedeem(address owner) external view returns (uint);
// Preview
function previewDeposit(uint assets) external view returns (uint);
function previewMint(uint shares) external view returns (uint);
function previewWithdraw(uint assets) external view returns (uint);
function previewRedeem(uint shares) external view returns (uint);
}
Implementation
Step 1: Basic Structure
import {ERC20} from "solmate/tokens/ERC20.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
contract SimpleVault is ERC20 {
IERC20 public immutable asset;
constructor(
address _asset,
string memory _name,
string memory _symbol
) ERC20(_name, _symbol, ERC20(_asset).decimals()) {
asset = IERC20(_asset);
}
function totalAssets() public view returns (uint) {
return asset.balanceOf(address(this));
}
}
Step 2: Conversion Logic
function convertToShares(uint assets) public view returns (uint) {
uint supply = totalSupply;
return supply == 0 ? assets : (assets * supply) / totalAssets();
}
function convertToAssets(uint shares) public view returns (uint) {
uint supply = totalSupply;
return supply == 0 ? shares : (shares * totalAssets()) / supply;
}
महत्वपूर्ण: Division by zero से बचें।
Step 3: Deposit
function deposit(uint assets, address receiver) public returns (uint shares) {
// 1. Calculate shares
shares = convertToShares(assets);
// 2. Transfer assets
asset.transferFrom(msg.sender, address(this), assets);
// 3. Mint shares
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
function mint(uint shares, address receiver) public returns (uint assets) {
// Calculate assets needed
assets = convertToAssets(shares);
// Transfer and mint
asset.transferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
Step 4: Withdraw
function withdraw(
uint assets,
address receiver,
address owner
) public returns (uint shares) {
// Calculate shares to burn
shares = convertToShares(assets);
// Check allowance
if (msg.sender != owner) {
uint allowed = allowance[owner][msg.sender];
if (allowed != type(uint).max) {
allowance[owner][msg.sender] = allowed - shares;
}
}
// Burn shares
_burn(owner, shares);
// Transfer assets
asset.transfer(receiver, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
function redeem(
uint shares,
address receiver,
address owner
) public returns (uint assets) {
// Calculate assets to return
assets = convertToAssets(shares);
// Check allowance
if (msg.sender != owner) {
uint allowed = allowance[owner][msg.sender];
if (allowed != type(uint).max) {
allowance[owner][msg.sender] = allowed - shares;
}
}
// Burn and transfer
_burn(owner, shares);
asset.transfer(receiver, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
Step 5: Preview Functions
function previewDeposit(uint assets) public view returns (uint) {
return convertToShares(assets);
}
function previewMint(uint shares) public view returns (uint) {
uint supply = totalSupply;
return supply == 0 ? shares : (shares * totalAssets() + supply - 1) / supply;
}
function previewWithdraw(uint assets) public view returns (uint) {
uint supply = totalSupply;
return supply == 0 ? assets : (assets * supply + totalAssets() - 1) / totalAssets();
}
function previewRedeem(uint shares) public view returns (uint) {
return convertToAssets(shares);
}
Note: previewMint और previewWithdraw में rounding up होता है।
Step 6: Max Functions
function maxDeposit(address) public pure returns (uint) {
return type(uint).max;
}
function maxMint(address) public pure returns (uint) {
return type(uint).max;
}
function maxWithdraw(address owner) public view returns (uint) {
return convertToAssets(balanceOf[owner]);
}
function maxRedeem(address owner) public view returns (uint) {
return balanceOf[owner];
}
Complete Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "solmate/tokens/ERC20.sol";
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
contract Vault is ERC20 {
IERC20 public immutable asset;
event Deposit(address indexed sender, address indexed owner, uint assets, uint shares);
event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint assets, uint shares);
constructor(address _asset) ERC20("Vault Token", "vTKN", ERC20(_asset).decimals()) {
asset = IERC20(_asset);
}
function totalAssets() public view returns (uint) {
return asset.balanceOf(address(this));
}
function convertToShares(uint assets) public view returns (uint) {
uint supply = totalSupply;
return supply == 0 ? assets : (assets * supply) / totalAssets();
}
function convertToAssets(uint shares) public view returns (uint) {
uint supply = totalSupply;
return supply == 0 ? shares : (shares * totalAssets()) / supply;
}
function deposit(uint assets, address receiver) public returns (uint shares) {
shares = convertToShares(assets);
asset.transferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
emit Deposit(msg.sender, receiver, assets, shares);
}
function redeem(uint shares, address receiver, address owner) public returns (uint assets) {
if (msg.sender != owner) {
uint allowed = allowance[owner][msg.sender];
if (allowed != type(uint).max) allowance[owner][msg.sender] = allowed - shares;
}
assets = convertToAssets(shares);
_burn(owner, shares);
asset.transfer(receiver, assets);
emit Withdraw(msg.sender, receiver, owner, assets, shares);
}
// ... बाकी functions ...
}
Testing
contract VaultTest is Test {
Vault vault;
MockERC20 asset;
function setUp() public {
asset = new MockERC20();
vault = new Vault(address(asset));
}
function testDeposit() public {
asset.mint(address(this), 100e18);
asset.approve(address(vault), 100e18);
uint shares = vault.deposit(100e18, address(this));
assertEq(shares, 100e18);
assertEq(vault.totalAssets(), 100e18);
assertEq(vault.balanceOf(address(this)), 100e18);
}
function testWithdrawWithYield() public {
// Initial deposit
asset.mint(address(this), 100e18);
asset.approve(address(vault), 100e18);
vault.deposit(100e18, address(this));
// Simulate yield — vault में extra assets add करो
asset.mint(address(vault), 10e18); // 10% yield
// Withdraw
uint assets = vault.redeem(100e18, address(this), address(this));
assertEq(assets, 110e18); // 100 + 10 yield
}
}
Advanced: Yield Strategy
Simple vault idle assets hold करता है। Real vaults invest करते हैं:
contract YieldVault is Vault {
IYieldStrategy public strategy;
function deposit(uint assets, address receiver) public override returns (uint shares) {
shares = super.deposit(assets, receiver);
// Invest in strategy
asset.approve(address(strategy), assets);
strategy.deposit(assets);
}
function totalAssets() public view override returns (uint) {
return asset.balanceOf(address(this)) + strategy.balanceOf(address(this));
}
}
Real-World Examples
- Yearn Finance: Multi-strategy vaults
- Aave V3: aTokens implementing ERC-4626
- Balancer: Boosted pools
निष्कर्ष
ERC-4626 powerful standard है:
- Composable (vaults of vaults)
- Standardized (uniform interface)
- Capital efficient
Implementation सीधी है, लेकिन edge cases (first depositor, rounding) ध्यान मांगते हैं।
Production वaults में:
- Access control add करें
- Fees implement करें (management, performance)
- Emergency pause mechanism
- Multiple strategies support
Next step: OpenZeppelin का ERC4626 contract study करें — battle-tested implementation।