# Building a Minimal ERC-4626 Vault from Scratch
ERC-4626 standardizes tokenized vaults. Users deposit assets, receive shares representing proportional ownership.
Core Concept
Deposit: 100 USDC -> 100 shares (1:1)
Yield accrues: vault has 110 USDC for 100 shares
Withdraw: 100 shares -> 110 USDC
Implementation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract SimpleVault is ERC20 {
using SafeERC20 for IERC20;
IERC20 public immutable asset;
constructor(IERC20 _asset) ERC20("Vault Shares", "vSHARE") {
asset = _asset;
}
function totalAssets() public view returns (uint256) {
return asset.balanceOf(address(this));
}
function convertToShares(uint256 assets) public view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? assets : (assets * supply) / totalAssets();
}
function convertToAssets(uint256 shares) public view returns (uint256) {
uint256 supply = totalSupply();
return supply == 0 ? shares : (shares * totalAssets()) / supply;
}
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
shares = convertToShares(assets);
require(shares > 0, "Zero shares");
asset.safeTransferFrom(msg.sender, address(this), assets);
_mint(receiver, shares);
}
function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets) {
assets = convertToAssets(shares);
if (msg.sender != owner) _spendAllowance(owner, msg.sender, shares);
_burn(owner, shares);
asset.safeTransfer(receiver, assets);
}
}
Security: Inflation Attack
The first depositor can manipulate share price. Mitigation:
constructor(IERC20 _asset) ERC20("Vault", "vTKN") {
asset = _asset;
_mint(address(1), 1000); // dead shares
}