Tutorial·9 min का पठन·Solingo द्वारा

Scratch से Minimal ERC-4626 Vault बनाएं

Tokenized vaults का standard। Step-by-step implementation guide with shares accounting और yield distribution।

# 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।

Practice में लगाने के लिए तैयार हैं?

Solingo पर interactive exercises के साथ इन concepts को apply करें।

मुफ्त में शुरू करें