Vault

Vault(保险库) 通常指一种资金管理和收益策略工具,主要用于 DeFi(去中心化金融)协议中,用来帮助用户自动化收益最大化,降低操作难度和 Gas 成本。

Vault 的主要功能

  1. 自动复投
    Vault 会将挖矿奖励、利息等收益,自动复投到原策略中,提升复利效应,用户不需要手动操作。
  2. 策略自动化
    比如 Yearn Finance 的 Vault,会根据链上数据动态调整策略,将资金投向收益更高的 DeFi 协议。
  3. 降低 Gas 成本
    单个用户频繁操作会耗费大量 Gas,而 Vault 将资金集中,批量执行操作,摊薄 Gas 成本。
  4. 风险隔离
    不同策略可以部署在不同的 Vault 中,用户可以根据风险偏好选择。
  5. yield farming/借贷/质押

Yield Farming(收益耕作)DeFi(去中心化金融) 中的一种策略,指用户将自己的加密资产存入特定协议(如借贷、流动性池、收益聚合器)中,以赚取额外收益(通常是利息或平台代币奖励)的行为。

简单来说,Yield Farming 就是“把加密货币拿去生钱”,类似于传统金融里的“存款生息 + 活期理财”,但更复杂、收益更高、风险也更大。


典型项目

Yearn Finance
最经典的 Vault 应用,用户存入稳定币、ETH 或 LP Token,Vault 会自动将资金部署到 Curve、Compound 等平台,执行收益优化策略。

Beefy Finance
多链收益聚合器,Vault 帮助用户将流动性挖矿奖励复投,提升收益。


简单理解

如果把 DeFi 理解为“数字银行”,那么 Vault = 自动理财管家

  • 你把钱交给 Vault(智能合约)
  • 它帮你找高收益、做复投
  • 省去手动搬砖的麻烦

LP Token

LP Token(Liquidity Provider Token,流动性提供者代币)用户向去中心化交易所(DEX)或流动性池提供流动性后获得的凭证代币,它代表着用户在池子中所占的份额。

LP Token = 你在流动性池的股份证明,可以提币、分红、甚至去挖矿。


为什么会有 LP Token?

Uniswap、SushiSwap、Curve 等 AMM(自动做市商)模型的 DEX 中,交易对(如 ETH/USDC)需要资金池提供流动性。
当你往池子里存入等价值的两种资产(例如 1 ETH + 2000 USDC),你就成为了 流动性提供者(Liquidity Provider, LP)
为了证明你存入了多少,系统会给你发一个 LP Token,这就是你的 股份凭证


LP Token 的作用

  1. 提现凭证
    • 以后你要从池子里取出资金,必须用 LP Token 去兑换你的资产(本金 + 交易手续费分红)。
  2. 收益分配
    • 交易产生的手续费会按 LP Token 占比分给 LP 持有人。
  3. 参与 DeFi 挖矿(Liquidity Mining)
    • 你可以拿 LP Token 去 质押(Stake),获取额外奖励(比如治理代币)。

举例

  • 你往 Uniswap 的 ETH/USDC 池子存了 1 ETH + 2000 USDC
  • 系统给你 10 个 LP Token
  • 池子总价值是 100 ETH + 200,000 USDC(假设 LP Token 总量 1000)
  • 你的 LP Token 占比 = 10 / 1000 = 1%
  • 所以,你拥有池子中 1% 的资金份额和手续费收益

风险

  • 无常损失(Impermanent Loss):如果 ETH 价格大涨或大跌,你最终取出的资产组合可能少于单纯持币的价值。
  • 合约风险:LP Token 本身是由智能合约发行,如果合约被攻击,你可能损失资金。
  • 代币贬值:如果奖励代币暴跌,实际收益降低。

Vault01实验

目的

实现一个基础的Vault合约,并结合ERC20代币,完成用户的充值(deposit)取款(withdraw)逻辑。

准备

1. USDT合约

合约路径ERC20_fortest.sol
功能描述
该合约基于 OpenZeppelin 提供的 ERC20 标准,实现了一个名为 Tether USD(USDT) 的代币,主要用于模拟稳定币的发行。

代码
1
2
3
4
5
6
7
8
9
10
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract USDT is ERC20 {
constructor(uint256 initialSupply) ERC20("Tether USD", "USDT") {
_mint(msg.sender, initialSupply);
}
}

功能特点

  • 使用 OpenZeppelin ERC20 标准库,安全且兼容性强。
  • 构造函数中通过 _mint() 为部署者铸造初始代币。

2. Vault01合约

合约路径Vault01.sol
功能描述
该合约实现一个资金池(Vault),用户可以存入 ERC20 代币并获得对应的 shares,shares 代表用户在池中的份额。用户可通过 shares 按比例赎回对应的代币。

核心逻辑

  • 存款(deposit)
    用户将代币转入合约地址,并按规则获得对应份额(shares)。
1
2
3
4
5
6
7
8
9
/*  
a = amount
B = balance of token before deposit
T = share total supply
s = shares to mint

(T + s) / T = (a + B) / B
s = aT / B
*/
  • 取款(withdraw)
1
2
3
4
5
6
7
8
9
10
/*  
a = amount
B = balance of token before deposit
T = share total supply
s = shares to redeem
 
 
(T - s) / T = (B - a) / B  
a = sB / T  
*/

核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/interfaces/IERC20.sol";

contract Vault01 {
IERC20 public immutable token;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;

constructor(address _token) {
token = IERC20(_token);
}

function __mint(address _to, uint256 _shares) private {
totalSupply += _shares;
balanceOf[_to] += _shares;
}

function __burn(address _from, uint256 _shares) private {
totalSupply -= _shares;
balanceOf[_from] -= _shares;
}

function deposit(uint256 _amount) external {
uint256 shares;
if (totalSupply == 0) {
shares = _amount;
} else {
shares = (_amount * totalSupply) / token.balanceOf(address(this));
}
__mint(msg.sender, shares);
token.transferFrom(msg.sender, address(this), _amount);
}

function withdraw(uint256 _shares) external {
uint256 amount = (_shares * token.balanceOf(address(this))) / totalSupply;
__burn(msg.sender, _shares);
token.transfer(msg.sender, amount);
}
}

实验步骤

  1. 部署 USDT 合约

    • 在 Remix 中部署 ERC20_fortest.sol,初始发行 1000000 * 10^18 个代币。
    • 记录 USDT 合约地址,例如 0xABC...123
  2. 部署 Vault01 合约

    • 构造函数传入 USDT 合约地址 0xABC...123
    • Vault01 合约部署完成,等待交互。
  3. 授权 Vault01 合约转账

    • 在 USDT 合约中执行 approve(vaultAddress, amount)
    • 授权 Vault 可转账指定数量的 USDT。
  4. 测试存款

    • 调用 deposit(100e18)
    • 验证:
      • Vault01.balanceOf(msg.sender) 是否增加。
      • Vault01.totalSupply 是否更新。
      • Vault01.token.balanceOf(address(Vault01)) 是否为 100 USDT。
  5. 测试取款

    • 调用 withdraw(50e18)
    • 验证:
      • 用户账户 USDT 余额恢复。
      • Vault01.totalSupply 减少。

ERC4626阅读

assets & shares

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ERC4626 资产与份额接口

// 资产信息查询
/// @dev 返回金库的基础资产代币地址
function asset() external view returns (address assetTokenAddress);

/// @dev 返回金库管理的基础代币总额
function totalAssets() external view returns (uint256 totalManagedAssets);

// 数量转换
/// @dev 将资产数量转换为份额数量
//计算一个币可以转化成多少shares
function convertToShares(uint256 assets) external view returns (uint256 shares);

/// @dev 将份额数量转换为资产数量
function convertToAssets(uint256 shares) external view returns (uint256 assets);

包含两个核心功能组:

  • 资产信息查询(asset 和 totalAssets)
  • 资产与份额的相互转换(convertToShares 和 convertToAssets)

存款/铸造相关函数接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ERC4626 存款与铸造接口

// 最大可存款/铸造量查询
/// @dev 获取接收者可存入的最大资产数量
function maxDeposit(address receiver) external view returns (uint256 maxAssets);

/// @dev 获取接收者可铸造的最大份额数量
function maxMint(address receiver) external view returns (uint256 maxShares);

// 存款/铸造预览
/// @dev 预览存入指定资产数量将获得的份额数量
function previewDeposit(uint256 assets) external view returns (uint256 shares);

/// @dev 预览铸造指定份额数量需要的资产数量
function previewMint(uint256 shares) external view returns (uint256 assets);

// 存款/铸造操作
/// @dev 存入资产并给接收者铸造份额
function deposit(uint256 assets, address receiver) external returns (uint256 shares);

/// @dev 铸造份额并扣除接收者的资产
function mint(uint256 shares, address receiver) external returns (uint256 assets);

receiver的作用是什么?

receiver 参数的作用是 指定存款/铸造操作中接收份额(shares)的目标地址。它的设计目的是实现更灵活的资产托管和委托操作。

相当于充值给另外一个地址

msg.sender 的区别

  • msg.sender
    代表实际调用合约的地址(操作者),通常是支付资产(transferFrom)的地址。
  • receiver
    代表最终获得份额的地址,可能与 msg.sender 相同,也可能不同。

提款与赎回接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// ERC4626 提款与赎回接口

// ========== 最大可提款/赎回量查询 ==========
/// @dev 获取所有者可提取的最大资产数量(基于其份额)
function maxWithdraw(address owner) external view returns (uint256 maxAssets);

/// @dev 获取所有者可赎回的最大份额数量
function maxRedeem(address owner) external view returns (uint256 maxShares);

// ========== 提款/赎回预览计算 ==========
/// @dev 预览提取指定资产数量需要销毁的份额数量
function previewWithdraw(uint256 assets) external view returns (uint256 shares);

/// @dev 预览赎回指定份额数量将获得的资产数量
function previewRedeem(uint256 shares) external view returns (uint256 assets);

// ========== 提款/赎回操作 ==========
/// @dev 提取资产(销毁所有者的份额,资产发送给接收者)
function withdraw(
uint256 assets,
address receiver,
address owner
) external returns (uint256 shares);

/// @dev 赎回份额(销毁所有者的份额,资产发送给接收者)
function redeem(
uint256 shares,
address receiver,
address owner
) external returns (uint256 assets);

核心区别:withdraw vs redeem

  1. withdraw
    • 用户指定想要提取的资产数量assets)。
    • 系统自动计算需要销毁的份额(shares),通过 previewWithdraw 预览。
    • 适用场景:用户关注能拿到多少底层资产(如“我要提100 USDC”)。
  2. redeem
    • 用户指定要销毁的份额数量shares)。
    • 系统计算可获得的资产数量(assets),通过 previewRedeem 预览。
    • 适用场景:用户关注销毁多少份额(如“我要赎回50 vaultShares”)。

为什么需要 ownerreceiver

  1. owner
    • 代表实际持有份额的地址(必须已授权调用者操作其份额)。
    • 例如:智能合约代理可替用户操作,但份额属于用户地址。
  2. receiver
    • 允许将提取的资产发送到第三方地址(如归集钱包或另一个合约)。
    • 例如:用户可指定将资产直接发送到交易所地址。

总体:

image.png

两个事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ERC4626 存款/提款事件

/// @dev 当存入资产并铸造份额时触发
event Deposit(
address indexed sender, // 操作者地址(调用合约的地址)
address indexed owner, // 份额所有者地址
uint256 assets, // 存入的资产数量
uint256 shares // 铸造的份额数量
);

/// @dev 当提取资产并销毁份额时触发
event Withdraw(
address indexed sender, // 操作者地址(调用合约的地址)
address indexed receiver, // 资产接收者地址
address indexed owner, // 份额所有者地址
uint256 assets, // 提取的资产数量
uint256 shares // 销毁的份额数量
);

withdraw 操作中,如果 msg.sender !=owner那么 msg.sender 需要先请 owner 调用什么方法,才可以让 msg.sender 来 withdraw 成功?

答: approve

Front - Running

Front-Running 是指恶意节点(FrontRunner)通过监控待处理交易池(Mempool),以更高 Gas 费抢在目标交易(User’s Transaction)之前执行自己的交易,从而获利的行为。以下是分步拆解:


1. 用户发起交易

  • 用户提交一笔交易(如购买代币、调用智能合约)到以太坊网络。
  • 交易进入待处理池(Mempool),等待矿工/验证者打包。

2. 恶意节点监控交易池

  • FrontRunner(通常是机器人)实时扫描Mempool,识别有利可图的交易:
    • 例如:用户的大额代币买单(可能推高价格)。
    • 或套利机会(如DEX价格偏差)。

3. 发起抢先交易

  • 策略
    • 复制用户交易逻辑(如购买同种代币)。
    • 设置更高Gas费,吸引矿工优先打包。
  • 结果
    • 恶意交易被优先执行,用户交易因Gas低而延后。

4. 获利手段

  • 低买高卖:抢在用户买单前低价购入代币,待用户交易推高价格后卖出。
  • 套利:利用DEX价格延迟,在用户交易前完成价差套利。
  • 操纵合约:针对拍卖类合约,提前锁定优势条件。

技术实现依赖

  1. 交易透明性:以太坊Mempool公开可见,便于监控。
  2. Gas竞价机制:矿工优先打包高Gas交易。
  3. 智能合约可预测性:若合约逻辑固定,攻击者可模拟结果。

防御方案

  • 隐私交易:使用Flashbots等隐私RPC,避免交易暴露在公开Mempool。
  • 限价单/滑点控制:设置交易价格上限,减少被利用空间。
  • 合约级防护
    • 提交-揭示模式(Commit-Reveal Scheme)。
    • 随机化关键操作(如拍卖截止时间)。

典型案例

  • DeFi套利:2020年Uniswap上多次出现抢跑套利,单笔获利超万美元。
  • NFT铸造:热门项目公售时,机器人抢先铸造稀缺资产。

Front-Running本质是利用区块链透明性和激励机制的设计缺陷,是Web3中典型的”黑暗森林”攻击。

ERC4626 通胀攻击(Inflation Attack)

攻击背景

ERC4626 是代币化金库(Vault)标准,允许用户存入资产(assets)并获取份额(shares)。攻击者通过操纵金库的 资产-份额转换比例,在特定条件下实现“凭空增发份额”的漏洞。


攻击场景示例

假设初始状态:

  • 用户存入资产:assets_deposited = 1,000
  • 当前总份额:totalSupply() = 1,000
  • 金库总资产:totalAssets() = 1,000,000
    此时,用户预期获得的份额应通过公式计算:
    1
    2
    shares_received = assets_deposited * totalSupply() / totalAssets()
    = 1,000 * 1,000 / 1,000,000 = 1 share

漏洞触发点

问题出在 _convertToShares 函数的实现:

1
2
3
4
5
6
7
8
function _convertToShares(uint256 assets, Math.Rounding rounding) 
internal view virtual returns (uint256) {
return assets.mulDiv(
totalSupply() + 10 ** _decimalsOffset(),
totalAssets() + 1, // 关键问题:分母被 +1 操纵
rounding
);
}

攻击步骤

  1. 初始状态:金库刚部署,totalSupply = 0totalAssets = 0
  2. 攻击者首次存款
    • 存入极少量资产(如 1 wei),此时:
      1
      shares = 1 * (0 + 1e18) / (0 + 1) = 1e18 shares
    • 攻击者以 1 wei 的成本获得 1e18 份额(天文数字)。
  3. 正常用户存款
    • 用户存入 1,000 资产时,因 totalSupply 被攻击者通胀:
      1
      shares = 1,000 * (1e18 + 1e18) / (1 + 1,000) ≈ 2e18 / 1,001 ≈ 2e15 shares
    • 用户实际获得的份额远低于预期,大部分价值被攻击者稀释。

攻击核心逻辑

  • 分母操纵totalAssets() + 1 的设计在初始状态下(totalAssets=0)会放大份额计算。
  • 份额通胀:攻击者通过极低成本获取大量份额,后续用户的存款被严重稀释。

防御方案

  1. 初始化保护

    • 在金库部署时预铸少量份额给零地址(如 1e18),避免 totalSupply=0 的极端情况。
      1
      2
      3
      4
      function __ERC4626_init(address asset, uint256 initialDeposit) internal {
      _mint(address(0), 1e18); // 预铸份额
      _deposit(initialDeposit, msg.sender); // 初始存款
      }
  2. 公式修正

    • 移除分母的 +1 逻辑,直接使用 assets * totalSupply / totalAssets
  3. 最小存款限制

    • 要求首次存款必须超过一定阈值(如 1e18 wei),提高攻击成本。

真实案例

  • 2022 年多个未实现防护的 ERC4626 金库(如某些收益聚合器)因此漏洞被攻击,导致用户份额价值被稀释。

总结:ERC4626 通胀攻击利用了初始状态下的数学漏洞,开发者需严格检查资产-份额转换公式的边界条件!

https://docs.openzeppelin.com/contracts/4.x/erc4626