迁移指南
迁移概述
从 Aave V3 迁移到 V4 需要考虑以下几个方面:
- 合约接口变化
- 数据结构更新
- 新功能适配
- 用户仓位迁移
接口变化
Pool 合约
solidity
// V3 接口
interface IPoolV3 {
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external;
}
// V4 接口 - 新增批量操作
interface IPoolV4 {
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external;
// 新增
function executeBatch(BatchOperation[] calldata operations) external;
}数据类型变化
solidity
// V3 ReserveData
struct ReserveDataV3 {
ReserveConfigurationMap configuration;
uint128 liquidityIndex;
uint128 currentLiquidityRate;
uint128 variableBorrowIndex;
uint128 currentVariableBorrowRate;
uint128 currentStableBorrowRate;
uint40 lastUpdateTimestamp;
uint16 id;
address aTokenAddress;
address stableDebtTokenAddress;
address variableDebtTokenAddress;
address interestRateStrategyAddress;
uint128 accruedToTreasury;
uint128 unbacked;
uint128 isolationModeTotalDebt;
}
// V4 ReserveData - 新增字段
struct ReserveDataV4 {
// ... 继承 V3 字段
// 新增
uint256 liquidityLayerAllocation;
uint256 dynamicLTV;
uint256 dynamicLiquidationThreshold;
bool softLiquidationEnabled;
}代码迁移
基础迁移
solidity
// V3 代码
contract MyProtocolV3 {
IPool public pool;
function deposit(address asset, uint256 amount) external {
IERC20(asset).transferFrom(msg.sender, address(this), amount);
IERC20(asset).approve(address(pool), amount);
pool.supply(asset, amount, msg.sender, 0);
}
}
// V4 代码 - 基本兼容
contract MyProtocolV4 {
IPoolV4 public pool;
function deposit(address asset, uint256 amount) external {
IERC20(asset).transferFrom(msg.sender, address(this), amount);
IERC20(asset).approve(address(pool), amount);
pool.supply(asset, amount, msg.sender, 0); // 接口相同
}
// 新增:利用批量操作优化
function depositMultiple(
address[] calldata assets,
uint256[] calldata amounts
) external {
BatchOperation[] memory ops = new BatchOperation[](assets.length);
for (uint i = 0; i < assets.length; i++) {
IERC20(assets[i]).transferFrom(msg.sender, address(this), amounts[i]);
IERC20(assets[i]).approve(address(pool), amounts[i]);
ops[i] = BatchOperation({
operationType: 0, // Supply
asset: assets[i],
amount: amounts[i],
interestRateMode: 0,
onBehalfOf: msg.sender
});
}
pool.executeBatch(ops);
}
}闪电贷迁移
solidity
// V3 闪电贷
contract FlashLoanV3 is FlashLoanSimpleReceiverBase {
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
// V3 逻辑
return true;
}
}
// V4 闪电贷 - 接口相同,但可以利用新功能
contract FlashLoanV4 is FlashLoanSimpleReceiverBaseV4 {
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external override returns (bool) {
// V4 逻辑 - 可以使用批量操作
BatchOperation[] memory ops = new BatchOperation[](2);
// ... 构建操作
POOL.executeBatch(ops);
return true;
}
}用户仓位迁移
迁移助手合约
solidity
pragma solidity ^0.8.20;
import {IPoolV3} from "@aave/core-v3/contracts/interfaces/IPool.sol";
import {IPoolV4} from "@aave/v4-core/contracts/interfaces/IPoolV4.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MigrationHelper {
IPoolV3 public immutable poolV3;
IPoolV4 public immutable poolV4;
constructor(address _poolV3, address _poolV4) {
poolV3 = IPoolV3(_poolV3);
poolV4 = IPoolV4(_poolV4);
}
/**
* @notice 迁移用户的供应仓位
* @param assets 要迁移的资产列表
*/
function migrateSupplyPositions(address[] calldata assets) external {
for (uint i = 0; i < assets.length; i++) {
_migrateSupply(assets[i]);
}
}
function _migrateSupply(address asset) internal {
// 1. 获取 V3 aToken 地址
DataTypes.ReserveData memory reserveData = poolV3.getReserveData(asset);
address aTokenV3 = reserveData.aTokenAddress;
// 2. 获取用户在 V3 的余额
uint256 balance = IERC20(aTokenV3).balanceOf(msg.sender);
if (balance == 0) return;
// 3. 从 V3 提取
// 用户需要先授权 aToken
IERC20(aTokenV3).transferFrom(msg.sender, address(this), balance);
uint256 withdrawn = poolV3.withdraw(asset, type(uint256).max, address(this));
// 4. 存入 V4
IERC20(asset).approve(address(poolV4), withdrawn);
poolV4.supply(asset, withdrawn, msg.sender, 0);
}
/**
* @notice 迁移完整仓位(包括债务)
* @dev 使用闪电贷来处理债务迁移
*/
function migrateFullPosition(
address[] calldata supplyAssets,
address[] calldata debtAssets,
uint256[] calldata debtAmounts
) external {
// 使用闪电贷偿还 V3 债务
bytes memory params = abi.encode(
msg.sender,
supplyAssets,
debtAssets,
debtAmounts
);
// 从 V4 借出资金来偿还 V3 债务
poolV4.flashLoanSimple(
address(this),
debtAssets[0], // 简化:假设单一债务资产
debtAmounts[0],
params,
0
);
}
function executeOperation(
address asset,
uint256 amount,
uint256 premium,
address initiator,
bytes calldata params
) external returns (bool) {
require(msg.sender == address(poolV4), "Invalid caller");
(
address user,
address[] memory supplyAssets,
address[] memory debtAssets,
uint256[] memory debtAmounts
) = abi.decode(params, (address, address[], address[], uint256[]));
// 1. 偿还 V3 债务
for (uint i = 0; i < debtAssets.length; i++) {
IERC20(debtAssets[i]).approve(address(poolV3), debtAmounts[i]);
poolV3.repay(debtAssets[i], debtAmounts[i], 2, user);
}
// 2. 从 V3 提取抵押品
for (uint i = 0; i < supplyAssets.length; i++) {
DataTypes.ReserveData memory data = poolV3.getReserveData(supplyAssets[i]);
uint256 balance = IERC20(data.aTokenAddress).balanceOf(user);
if (balance > 0) {
// 用户需要预先授权
IERC20(data.aTokenAddress).transferFrom(user, address(this), balance);
poolV3.withdraw(supplyAssets[i], type(uint256).max, address(this));
}
}
// 3. 存入 V4
for (uint i = 0; i < supplyAssets.length; i++) {
uint256 balance = IERC20(supplyAssets[i]).balanceOf(address(this));
if (balance > 0) {
IERC20(supplyAssets[i]).approve(address(poolV4), balance);
poolV4.supply(supplyAssets[i], balance, user, 0);
}
}
// 4. 在 V4 重新借贷
for (uint i = 0; i < debtAssets.length; i++) {
poolV4.borrow(debtAssets[i], debtAmounts[i], 2, 0, user);
}
// 5. 偿还闪电贷
uint256 totalDebt = amount + premium;
IERC20(asset).approve(address(poolV4), totalDebt);
return true;
}
/**
* @notice 估算迁移成本
*/
function estimateMigrationCost(
address user,
address[] calldata supplyAssets,
address[] calldata debtAssets
) external view returns (uint256 estimatedGas) {
// 基础 gas 成本
estimatedGas = 100000;
// 每个供应资产的成本
estimatedGas += supplyAssets.length * 150000;
// 每个债务资产的成本
estimatedGas += debtAssets.length * 200000;
// 闪电贷成本
if (debtAssets.length > 0) {
estimatedGas += 100000;
}
}
}JavaScript 迁移脚本
typescript
import { ethers } from 'ethers';
class AaveMigrator {
private poolV3: ethers.Contract;
private poolV4: ethers.Contract;
private migrationHelper: ethers.Contract;
private signer: ethers.Signer;
constructor(
poolV3Address: string,
poolV4Address: string,
migrationHelperAddress: string,
signer: ethers.Signer
) {
this.poolV3 = new ethers.Contract(poolV3Address, POOL_V3_ABI, signer);
this.poolV4 = new ethers.Contract(poolV4Address, POOL_V4_ABI, signer);
this.migrationHelper = new ethers.Contract(
migrationHelperAddress,
MIGRATION_HELPER_ABI,
signer
);
this.signer = signer;
}
async analyzePosition(): Promise<{
supplies: { asset: string; amount: string }[];
debts: { asset: string; amount: string }[];
healthFactor: string;
}> {
const userAddress = await this.signer.getAddress();
// 获取 V3 账户数据
const accountData = await this.poolV3.getUserAccountData(userAddress);
// 获取所有储备
const reserves = await this.poolV3.getReservesList();
const supplies: { asset: string; amount: string }[] = [];
const debts: { asset: string; amount: string }[] = [];
for (const reserve of reserves) {
const reserveData = await this.poolV3.getReserveData(reserve);
// 检查供应
const aToken = new ethers.Contract(
reserveData.aTokenAddress,
ERC20_ABI,
this.signer
);
const supplyBalance = await aToken.balanceOf(userAddress);
if (supplyBalance.gt(0)) {
supplies.push({
asset: reserve,
amount: supplyBalance.toString()
});
}
// 检查债务
const debtToken = new ethers.Contract(
reserveData.variableDebtTokenAddress,
ERC20_ABI,
this.signer
);
const debtBalance = await debtToken.balanceOf(userAddress);
if (debtBalance.gt(0)) {
debts.push({
asset: reserve,
amount: debtBalance.toString()
});
}
}
return {
supplies,
debts,
healthFactor: ethers.utils.formatEther(accountData.healthFactor)
};
}
async migrate(): Promise<string[]> {
const position = await this.analyzePosition();
const txHashes: string[] = [];
console.log('Current Position:', position);
// 1. 授权 aTokens
for (const supply of position.supplies) {
const reserveData = await this.poolV3.getReserveData(supply.asset);
const aToken = new ethers.Contract(
reserveData.aTokenAddress,
ERC20_ABI,
this.signer
);
const tx = await aToken.approve(
this.migrationHelper.address,
ethers.constants.MaxUint256
);
await tx.wait();
txHashes.push(tx.hash);
console.log(`Approved aToken for ${supply.asset}`);
}
// 2. 执行迁移
if (position.debts.length === 0) {
// 无债务,简单迁移
const supplyAssets = position.supplies.map(s => s.asset);
const tx = await this.migrationHelper.migrateSupplyPositions(supplyAssets);
await tx.wait();
txHashes.push(tx.hash);
console.log('Supply positions migrated');
} else {
// 有债务,完整迁移
const supplyAssets = position.supplies.map(s => s.asset);
const debtAssets = position.debts.map(d => d.asset);
const debtAmounts = position.debts.map(d => d.amount);
const tx = await this.migrationHelper.migrateFullPosition(
supplyAssets,
debtAssets,
debtAmounts
);
await tx.wait();
txHashes.push(tx.hash);
console.log('Full position migrated');
}
return txHashes;
}
async verifyMigration(): Promise<boolean> {
const userAddress = await this.signer.getAddress();
// 检查 V3 仓位是否清空
const v3Data = await this.poolV3.getUserAccountData(userAddress);
const v3Empty = v3Data.totalCollateralBase.eq(0) && v3Data.totalDebtBase.eq(0);
// 检查 V4 仓位
const v4Data = await this.poolV4.getUserAccountData(userAddress);
const v4HasPosition = v4Data.totalCollateralBase.gt(0);
console.log('V3 Position Cleared:', v3Empty);
console.log('V4 Position Created:', v4HasPosition);
return v3Empty && v4HasPosition;
}
}
// 使用示例
async function main() {
const provider = new ethers.providers.JsonRpcProvider('YOUR_RPC_URL');
const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);
const migrator = new AaveMigrator(
POOL_V3_ADDRESS,
POOL_V4_ADDRESS,
MIGRATION_HELPER_ADDRESS,
signer
);
// 分析当前仓位
const position = await migrator.analyzePosition();
console.log('Current Position:', position);
// 执行迁移
const txHashes = await migrator.migrate();
console.log('Migration Transactions:', txHashes);
// 验证迁移
const success = await migrator.verifyMigration();
console.log('Migration Successful:', success);
}迁移检查清单
迁移前
迁移中
迁移后
常见问题
Q: 迁移会影响我的健康因子吗?
A: 迁移过程中健康因子可能会暂时波动,但最终应该保持相似水平。V4 的动态风险参数可能会导致略有不同的健康因子。
Q: 迁移需要多少 Gas?
A: 取决于仓位复杂度:
- 简单供应仓位:约 200,000-300,000 Gas
- 包含债务的仓位:约 500,000-800,000 Gas
Q: 可以部分迁移吗?
A: 可以,你可以选择只迁移部分资产。但如果有债务,建议一次性完整迁移。
Q: 迁移后可以回到 V3 吗?
A: 技术上可以,但不建议。V4 提供更好的功能和效率。
迁移前必读
- 在测试网充分测试迁移流程
- 确保有足够 Gas 完成整个迁移
- 选择市场波动较小的时间窗口
- 准备好紧急回滚方案