深度剖析 DeFAI 操作系统:基于 ERC4626 与 Chainlink 的多资产智能金库工程实现

6 阅读6分钟

一、 引言:当 DeFi 遇到人工智能(DeFAI)

传统的去中心化金融(DeFi)资产管理往往面临两大痛点:对于普通用户而言,跨链、调仓、多协议交互的操作门槛极高;对于专业交易员或量化策略而言,多链分散的流动性和高昂的信任成本阻碍了规模化资管的可能。

随着人工智能(AI)在 Web3 领域的爆发,DeFAI(DeFi + AI) 概念应运而生。其核心思想是:由 AI 智能体(AI Agents)通过意图(Intent-Based)引擎捕捉市场机会并组装复杂的链上指令,而由一个完全去中心化、非托管的链上金库底层负责资金的托管与安全清算。

本文将从架构设计、数学归一化清算、防夹子(MEV)攻击等理论层面出发,结合 Solidity 0.8.27OpenZeppelin v5 标准,深度解析一个工业级多资产智能金库(以类似 Velvet Capital 的底层架构为例)的完整工程实现。


二、 核心理论与架构设计

一个高可靠性的 DeFAI 资产管理系统,在底层智能合约设计上必须解决三个核心理论问题:标准化的份额通证化异构多资产的公允价值对齐,以及策略执行的权限与滑点控制

1. 资产代币化标准:为什么选择 ERC4626?

在 OpenZeppelin v5 中,ERC4626(代币化金库标准)已经成为行业共识。它继承自 ERC20,通过将用户存入的本位币(Underlying Asset,如 USDC)转化为代表金库权益的“份额代币(Shares)”,实现了收益率金库(Yield-bearing Vaults)的乐高积木式组合。
其核心数学公式为:

Shares to Mint=Assets Passed×Total Supply of SharesTotal Managed Assets\text{Shares to Mint} = \text{Assets Passed} \times \frac{\text{Total Supply of Shares}}{\text{Total Managed Assets}}

2. 多资产 NAV 清算理论与精度对齐

在实际的 DeFAI 业务中,AI 策略师调仓后,资金会散落在不同的投资标的中(如 WBTC、WETH、USDC)。此时,如何公允地计算金库的总资产净值(NAV,即合约中的 totalAssets())?
我们必须引入 Chainlink 价格预言机,并将异构资产(Decimals 不同、喂价精度不同)进行齐次化换算

设某资产持仓数量为 BaB_{a},其代币精度为 DaD_{a};该资产的 Chainlink 喂价为 PaP_{a},预言机精度为 DpaD_{pa}
金库本位币的 Chainlink 喂价为 PuP_{u},预言机精度为 DpuD_{pu},本位币自身代币精度为 DuD_{u}

则该资产折算为本位币数量的公式为:

Value in USD=Ba×Pa10Dpa\text{Value in USD} = \frac{B_{a} \times P_{a}}{10^{D_{pa}}}

Value in Underlying=Value in USD×10DpuPu×10(DuDa)\text{Value in Underlying} = \frac{\text{Value in USD} \times 10^{D_{pu}}}{P_{u}} \times 10^{(D_{u} - D_{a})}

在 Solidity 编写中,为了防止整数除法导致的精度丢失(Precision Loss)或乘法引起的数值溢出(Overflow),必须严格按照“先乘后除”以及“动态缩放精度阶梯”的顺序进行位移计算。

3. 三层预言机安全断言(防范陈旧喂价攻击)

价格预言机是黑客攻击的重灾区。合约在读取 Chainlink 数据时,必须引入三层防护断言:

  1. 零值/负值校验:防止预言机因极度流动性匮乏返回异常零价。
  2. 心跳时效校验(Heartbeat Check) :验证 block.timestamp - updatedAt <= HEARTBEAT_THRESHOLD,防止在网络拥堵、节点故障或链硬分叉时,黑客利用过时的陈旧价格进行低成本套利(Stale Price Attack)。
  3. Round 完整性校验:确保 answeredInRound >= roundId,杜绝未完成聚合的抢跑价格。

三、 工业级核心智能合约实现

基于以上理论,我们构建了一个完整的智能合约系统,包含多资产 Chainlink 喂价路由、防 MEV 滑点控制的策略执行器,以及符合 OpenZeppelin v5 规范的收益表现费(Performance Fee)管理模块。

1. 核心多资产预言机金库(VelvetVaultWithOracle.sol)

核心合约 VelvetVaultWithOracle 继承自 ERC4626,利用 Chainlink 喂价实现多资产价值归一化。核心功能包括通过 totalAssets() 计算 NAV,以及通过 executeStrategy() 执行策略。为了保障安全,合约集成了 ReentrancyGuard 并在调仓时通过 minExpectedBalances 防御 MEV 攻击。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

// 引入 Chainlink 官方通用价格聚合器接口
interface AggregatorV3Interface {
    function decimals() external view returns (uint8);
    function latestRoundData()
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );
}

/**
 * @title VelvetVaultWithOracle
 * @dev 集成 Chainlink 喂价的多资产非托管金库,全面支持 AI/交易员策略执行与业绩收益费分润。
 * 完美适配 OpenZeppelin v5 与 Solidity 0.8.27。
 */
contract VelvetVaultWithOracle is ERC4626, Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // --- 状态变量 ---
    address public strategist;
    uint256 public performanceFeeBps;           // 业绩表现手续费,单位:基点 (1 bps = 0.01%)
    uint256 public constant MAX_FEE_BPS = 1000;  // 表现手续费硬上限 10%
    
    // 允许金库参与的多资产白名单路由
    address[] public allowedAssets;
    mapping(address => bool) public isAssetAllowed;
    mapping(address => uint8) public assetDecimals; // 资产精度缓存,深度节省链上 Gas

    // 资产对应的 Chainlink 喂价预言机地址 (Asset => PriceFeed)
    // 注意:注册进来的所有预言机,其计价本位必须保持齐次(例如均为美元计价 USD,或均为 ETH 计价)
    mapping(address => address) public priceFeeds;
    
    // 预言机数据过期时间阈值(24小时 = 86400 秒),防止遭遇极端黑天鹅事件时的陈旧价格套利
    uint256 public constant HEARTBEAT_THRESHOLD = 86400; 

    // --- 异常定义 ---
    error AssetNotAllowed(address asset);
    error InvalidFeeConfiguration();
    error UnauthorizedStrategist();
    error SlippageExceeded();
    error ArrayLengthMismatch();
    error StalePriceFeed(address asset);
    error InvalidPrice(address asset);

    // --- 事件定义 ---
    event StrategyExecuted(address[] targets, bytes[] datas, uint256[] minExpectedBalances);
    event PriceFeedUpdated(address indexed asset, address indexed priceFeed);
    event PerformanceFeeUpdated(uint256 oldFee, uint256 newFee);

    /**
     * @dev 构造函数遵循 OpenZeppelin v5 规范,直接向底层 Ownable 传递 initialOwner。
     */
    constructor(
        IERC20 _underlyingAsset, // 金库本位币(如 USDC,充当存入和取出时的基准计算资产)
        string memory _name,
        string memory _symbol,
        address _initialOwner,
        address _strategist,
        address[] memory _allowedAssets,
        address[] memory _initialFeeds
    ) 
        ERC20(_name, _symbol) 
        ERC4626(_underlyingAsset) 
        Ownable(_initialOwner) 
    {
        if (_allowedAssets.length != _initialFeeds.length) revert ArrayLengthMismatch();
        strategist = _strategist;
        
        // 自动初始化本位币在精度缓存中的设置
        assetDecimals[address(_underlyingAsset)] = ERC20(address(_underlyingAsset)).decimals();

        for (uint256 i = 0; i < _allowedAssets.length; i++) {
            address asset = _allowedAssets[i];
            address feed = _initialFeeds[i];
            
            if (asset == address(0) || feed == address(0)) revert AssetNotAllowed(address(0));
            
            allowedAssets.push(asset);
            isAssetAllowed[asset] = true;
            priceFeeds[asset] = feed;
            assetDecimals[asset] = ERC20(asset).decimals();
            
            emit PriceFeedUpdated(asset, feed);
        }
    }

    // --- 修饰符 ---
    modifier typeStrategistOrOwner() {
        if (msg.sender != strategist && msg.sender != owner()) revert UnauthorizedStrategist();
        _;
    }

    // --- 核心逻辑重写:动态计算多资产总净值 (NAV) ---

    /**
     * @notice 计算金库当前持有的所有白名单多资产的总价值(通过预言机无缝折算为本位币数量)
     */
    function totalAssets() public view override returns (uint256) {
        address underlying = asset();
        uint256 totalValueInUnderlying = 0;
        
        // 1. 累加金库内现存的未出资的本位币余额
        totalValueInUnderlying += IERC20(underlying).balanceOf(address(this));

        // 2. 遍历并计算已调仓投资到其他白名单代币中的持仓价值
        for (uint256 i = 0; i < allowedAssets.length; i++) {
            address currentAsset = allowedAssets[i];
            
            // 跳过本位币本身的重复叠加计算
            if (currentAsset == underlying) continue;

            uint256 assetBalance = IERC20(currentAsset).balanceOf(address(this));
            if (assetBalance == 0) continue;

            // 读取两端资产的最新的预言机价格与精度
            (uint256 price, uint8 feedDecimals) = _getAssetPrice(currentAsset);
            (uint256 underlyingPrice, uint8 underlyingFeedDecimals) = _getAssetPrice(underlying);

            // 工业级齐次化换算公式:
            // 先通过除以各自预言机精度将持仓折算为虚拟的归一化 USD 额度,再通过本位币价格折合回本位币对应的数量
            uint256 assetValueInUSD = (assetBalance * price) / (10 ** feedDecimals);
            uint256 assetValueInUnderlying = (assetValueInUSD * (10 ** underlyingFeedDecimals)) / underlyingPrice;
            
            // 消除不同代币本身(如 8位 WBTC 与 6位 USDC)固有的 Decimals 精度阶梯差
            uint8 currentDecimals = assetDecimals[currentAsset];
            uint8 underlyingDecimals = assetDecimals[underlying];
            
            if (underlyingDecimals >= currentDecimals) {
                assetValueInUnderlying = assetValueInUnderlying * (10 ** (underlyingDecimals - currentDecimals));
            } else {
                assetValueInUnderlying = assetValueInUnderlying / (10 ** (currentDecimals - underlyingDecimals));
            }

            totalValueInUnderlying += assetValueInUnderlying;
        }

        return totalValueInUnderlying;
    }

    // --- 核心 AI / 交易员 意图调仓策略执行入口 ---
    
    /**
     * @notice 执行复杂的外部多步链上路由交互(例如去中心化交易所兑换、流动性质押等)
     */
    function executeStrategy(
        address[] calldata targets,
        bytes[] calldata datas,
        uint256[] calldata minExpectedBalances
    ) external typeStrategistOrOwner nonReentrant {
        if (targets.length != datas.length) revert ArrayLengthMismatch();

        for (uint256 i = 0; i < targets.length; i++) {
            // 利用低级调用安全拼接 DeFi 乐高积木
            (bool success, bytes memory result) = targets[i].call(datas[i]);
            if (!success) {
                assembly {
                    revert(add(32, result), mload(result))
                }
            }
        }

        // 交易完毕后的实时多重资产滑点核验(防夹子与恶意抢跑攻击)
        if (minExpectedBalances.length > 0) {
            if (minExpectedBalances.length != allowedAssets.length) revert ArrayLengthMismatch();
            for (uint256 i = 0; i < allowedAssets.length; i++) {
                uint256 currentBalance = IERC20(allowedAssets[i]).balanceOf(address(this));
                if (currentBalance < minExpectedBalances[i]) revert SlippageExceeded();
            }
        }

        emit StrategyExecuted(targets, datas, minExpectedBalances);
    }

    // --- 收益费与系统管理功能 ---

    /**
     * @notice 动态调整表现手续费率(仅限金库所有者)
     * @param _newFeeBps 新的费用基点(最大不可超过 1000,即 10%)
     */
    function setPerformanceFee(uint256 _newFeeBps) external onlyOwner {
        if (_newFeeBps > MAX_FEE_BPS) revert InvalidFeeConfiguration();
        emit PerformanceFeeUpdated(performanceFeeBps, _newFeeBps);
        performanceFeeBps = _newFeeBps;
    }

    /**
     * @notice 自动化归集并提现留存在金库内的策略费资产至国库
     * @param asset 提现的资产代币地址
     * @param receiver 接收费用资产的国库或多签钱包地址
     */
    function withdrawFees(address asset, address receiver) external onlyOwner {
        uint256 balance = IERC20(asset).balanceOf(address(this));
        IERC20(asset).safeTransfer(receiver, balance);
    }

    /**
     * @notice 动态调整或拓展白名单资产的价格预言机组件
     */
    function updatePriceFeed(address _asset, address _priceFeed) external onlyOwner {
        if (_asset == address(0) || _priceFeed == address(0)) revert AssetNotAllowed(address(0));
        
        if (!isAssetAllowed[_asset]) {
            allowedAssets.push(_asset);
            isAssetAllowed[_asset] = true;
            assetDecimals[_asset] = ERC20(_asset).decimals();
        }
        
        priceFeeds[_asset] = _priceFeed;
        emit PriceFeedUpdated(_asset, _priceFeed);
    }

    // --- 内部辅助函数:安全校验并读取 Chainlink 喂价数据 ---
    function _getAssetPrice(address _asset) internal view returns (uint256, uint8) {
        address feedAddress = priceFeeds[_asset];
        if (feedAddress == address(0)) revert AssetNotAllowed(_asset);

        AggregatorV3Interface priceFeed = AggregatorV3Interface(feedAddress);
        
        (
            uint80 roundId,
            int256 answer,
            ,
            uint256 updatedAt,
            uint80 answeredInRound
        ) = priceFeed.latestRoundData();

        if (answer <= 0) revert InvalidPrice(_asset);
        if (block.timestamp - updatedAt > HEARTBEAT_THRESHOLD) revert StalePriceFeed(_asset);
        if (answeredInRound < roundId) revert StalePriceFeed(_asset);

        return (uint256(answer), priceFeed.decimals());
    }
}

2. 金库确定性部署工厂(VelvetVaultFactory.sol)

VelvetVaultFactory 用于工厂化、确定性地部署 VelvetVaultWithOracle 合约,支持创建多资产、不同 AI 策略的子金库。它记录了所有部署的 Vault,并支持按用户查询。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {VelvetVaultWithOracle} from "./VelvetVaultWithOracle.sol"; // 确保指向正确的子金库

contract VelvetVaultFactory is Ownable {
    address[] public allVaults;
    mapping(address => bool) public isVaultDeployedByFactory;
    mapping(address => address[]) public userVaults;

    event VaultCreated(address indexed vaultAddress, address indexed owner, address indexed strategist, address underlying, uint256 index);

    constructor(address _initialOwner) Ownable(_initialOwner) {}

    // 💡 修复:升级工厂函数,增加第6个参数:_initialFeeds
    function createVault(
        address _underlyingAsset,
        string calldata _name,
        string calldata _symbol,
        address _strategist,
        address[] calldata _allowedAssets,
        address[] calldata _initialFeeds // 👈 新增此参数
    ) external returns (address) {
        
        // 实例化时正确透传 6 个参数给子金库
        VelvetVaultWithOracle newVault = new VelvetVaultWithOracle(
            IERC20(_underlyingAsset),
            _name,
            _symbol,
            msg.sender, // 设为金库的初始 owner
            _strategist,
            _allowedAssets,
            _initialFeeds
        );

        address vaultAddress = address(newVault);

        allVaults.push(vaultAddress);
        isVaultDeployedByFactory[vaultAddress] = true;
        userVaults[msg.sender].push(vaultAddress);

        emit VaultCreated(vaultAddress, msg.sender, _strategist, _underlyingAsset, allVaults.length - 1);

        return vaultAddress;
    }

    function getUserVaults(address _user) external view returns (address[] memory) {
        return userVaults[_user];
    }
}

3. 核心金库合约(VelvetVault.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {IERC20, ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title VelvetVault
 * @dev 基于 OpenZeppelin v5 实现的非托管代币化资产组合金库。
 * 支持特定策略师(Strategist)进行自动化资产调仓,完美贴合 DeFAI 意图执行场景。
 */
contract VelvetVault is ERC4626, Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    // --- 状态变量 ---
    address public strategist;
    uint256 public performanceFeeBps; // 业绩手续费,单位:基点 (1 bps = 0.01%)
    uint256 public constant MAX_FEE_BPS = 1000; // 手续费上限 10%

    // 允许金库投资的底层白名单资产组合
    address[] public allowedAssets;
    mapping(address => bool) public isAssetAllowed;

    // --- 异常定义 ---
    error AssetNotAllowed(address asset);
    error InvalidFeeConfiguration();
    error UnauthorizedStrategist();
    error SlippageExceeded();
    error ArrayLengthMismatch();

    // --- 事件定义 ---
    event StrategyExecuted(address indexed target, bytes data, uint256 timestamp);
    event StrategistUpdated(address indexed oldStrategist, address indexed newStrategist);
    event PerformanceFeeUpdated(uint256 oldFee, uint256 newFee);

    /**
     * @dev 构造函数遵循 OpenZeppelin v5 规范,直接向 Ownable 传递 initialOwner。
     */
    constructor(
        IERC20 _underlyingAsset,
        string memory _name,
        string memory _symbol,
        address _initialOwner,
        address _strategist,
        address[] memory _allowedAssets
    ) 
        ERC20(_name, _symbol) 
        ERC4626(_underlyingAsset) 
        Ownable(_initialOwner) 
    {
        strategist = _strategist;
        
        for (uint256 i = 0; i < _allowedAssets.length; i++) {
            address asset = _allowedAssets[i];
            if (asset == address(0)) revert AssetNotAllowed(address(0));
            allowedAssets.push(asset);
            isAssetAllowed[asset] = true;
        }
    }

    // --- 修饰符 ---
    modifier typeStrategistOrOwner() {
        if (msg.sender != strategist && msg.sender != owner()) revert UnauthorizedStrategist();
        _;
    }

    // --- 核心 AI/交易员 意图策略执行入口 ---
    
    /**
     * @notice 执行链上交易意图(如去中心化交易所兑换、借贷、收益率挖矿等)
     * @dev 带有严格的资产白名单限制与防 MEV 的最小输出校验
     * @param targets 调用的目标外部 DeFi 智能合约地址数组
     * @param datas 经过编码后的底层交互 Calldata 数组
     * @param minExpectedBalances 调仓后,白名单内各资产应达到的最小底层资产余额(防夹子滑点控制)
     */
    function executeStrategy(
        address[] calldata targets,
        bytes[] calldata datas,
        uint256[] calldata minExpectedBalances
    ) external typeStrategistOrOwner nonReentrant {
        if (targets.length != datas.length) revert ArrayLengthMismatch();

        for (uint256 i = 0; i < targets.length; i++) {
            // 工业级安全实践:利用低级调用执行第三方 DeFi 乐高积木交互
            (bool success, bytes memory result) = targets[i].call(datas[i]);
            if (!success) {
                // 如果底层抛出异常,将原始错误信息向上冒泡
                assembly {
                    revert(add(32, result), mload(result))
                }
            }
            emit StrategyExecuted(targets[i], datas[i], block.timestamp);
        }

        // 交易后的滑点与资产完整性核验(防三层夹击或抢跑恶意耗尽资产)
        if (minExpectedBalances.length > 0) {
            if (minExpectedBalances.length != allowedAssets.length) revert ArrayLengthMismatch();
            for (uint256 i = 0; i < allowedAssets.length; i++) {
                uint256 currentBalance = IERC20(allowedAssets[i]).balanceOf(address(this));
                if (currentBalance < minExpectedBalances[i]) revert SlippageExceeded();
            }
        }
    }

    // --- 系统管理功能 ---

    function setStrategist(address _newStrategist) external onlyOwner {
        emit StrategistUpdated(strategist, _newStrategist);
        strategist = _newStrategist;
    }

    function setPerformanceFee(uint256 _newFeeBps) external onlyOwner {
        if (_newFeeBps > MAX_FEE_BPS) revert InvalidFeeConfiguration();
        emit PerformanceFeeUpdated(performanceFeeBps, _newFeeBps);
        performanceFeeBps = _newFeeBps;
    }

    /**
     * @notice 提取金库产生的管理或表现手续费
     */
    function withdrawFees(address asset, address receiver) external onlyOwner {
        uint256 balance = IERC20(asset).balanceOf(address(this));
        IERC20(asset).safeTransfer(receiver, balance);
    }

    // --- ERC4626 钩子重写 ---
    
    /**
     * @dev 获取金库总资产时,计算其底层计价代币的总价值
     */
    function totalAssets() public view override returns (uint256) {
        return IERC20(asset()).balanceOf(address(this));
    }
}

4. 模拟Chainlink喂价合约(MockChainlinkFeed.sol)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

contract MockChainlinkFeed {
    uint8 private _decimals;
    int256 private _price;
    uint256 private _updatedAt;

    constructor(uint8 decimals_, int256 initialPrice) {
        _decimals = decimals_;
        _price = initialPrice;
        _updatedAt = block.timestamp;
    }

    function decimals() external view returns (uint8) { return _decimals; }

    function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) {
        return (1, _price, _updatedAt, _updatedAt, 1);
    }

    function setMockRoundData(uint80, int256 price_, uint256 updatedAt_, uint256) external {
        _price = price_;
        _updatedAt = updatedAt_;
    }
}

5. 模拟代币智能合约 (TestUSDT.sol)

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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

/**
 * @dev 测试网专用 USDT,任意人都能 mint
 */
contract TestUSDT is ERC20 {
    uint8 private _decimals;

    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals_
    ) ERC20(name, symbol) {
        _decimals = decimals_;
    }

    function decimals() public view override returns (uint8) {
        return _decimals;
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

四、 自动化集成测试设计

为了模拟真实的 DeFAI 业务流水线,我们使用现代 Viem 库与 Node.js 原生的 node:test 测试框架,编写全流程覆盖脚本。测试用例涵盖了初始化验证、多资产 NAV 阶梯折算、心跳超时熔断拦截、以及管理表现费用的归集逻辑。

测试用例:Velvet Capital DeFAI Vault Protocol Integration

  • 工厂与金库初始化:应正确配置本位币、AI策略师以及Chainlink路由
  • ERC4626 资产存取流:投资者应能正常存入本位币并铸造份额
  • 多资产资产净值(NAV)计算:调仓产生多资产持仓后,预言机应能准确加权并归一化折算
  • 收益费流:金库应能正确调整表现费率,并在调仓获利后由管理员将留存手续费提取至国库
  • 安全风控拦截:防范超时陈旧预言机喂价(Stale Price)
  • 权限拦截:非策略师或Owner无法调用 executeStrategy 核心资产操作入口
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parseUnits, getAddress, encodeFunctionData } from "viem";
import { network } from "hardhat";

describe("Velvet Capital DeFAI Vault Protocol Integration", function () {
  
  async function deployFixture() {
    const { viem } = await (network as any).connect();
    const [owner, strategist, investor, maliciousUser, treasury] = await viem.getWalletClients();
    const publicClient = await viem.getPublicClient();

    // 1. 部署模拟资产 (USDC: 6位精度, WBTC: 8位精度)
    const mockUSDC = await viem.deployContract("TestUSDT", ["USD Coin", "USDC", 6]);
    const mockWBTC = await viem.deployContract("TestUSDT", ["Wrapped BTC", "WBTC", 8]);

    // 2. 部署 Mock 价格预言机
    const usdcFeed = await viem.deployContract("MockChainlinkFeed", [8, 100000000n]); // $1
    const wbtcFeed = await viem.deployContract("MockChainlinkFeed", [8, 6000000000000n]); // $60,000

    // 3. 部署工厂并一键初始化
    const factory = await viem.deployContract("VelvetVaultFactory", [owner.account.address]);

    const allowedAssets = [mockUSDC.address, mockWBTC.address];
    const initialFeeds = [usdcFeed.address, wbtcFeed.address];
    
    await factory.write.createVault([
      mockUSDC.address,
      "Velvet AI Aggressive Fund",
      "vAIAgg",
      strategist.account.address,
      allowedAssets,
      initialFeeds
    ], { account: owner.account });

    // 4. 获取部署后的金库地址并实例化
    const userVaults = await factory.read.getUserVaults([owner.account.address]);
    const vaultAddress = userVaults[0]; 
    const vaultContract = await viem.getContractAt("VelvetVaultWithOracle", vaultAddress);

    // 5. 为测试准备初始资金
    await mockUSDC.write.mint([investor.account.address, parseUnits("10000", 6)]);
    await mockWBTC.write.mint([owner.account.address, parseUnits("10", 8)]);

    return {
      factory, vaultContract, vaultAddress,
      mockUSDC, mockWBTC,
      usdcFeed, wbtcFeed,
      owner, strategist, investor, maliciousUser, treasury,
      publicClient, allowedAssets, initialFeeds
    };
  }

  it("工厂与金库初始化:应正确配置本位币、AI策略师以及Chainlink路由", async function () {
    const { vaultContract, mockUSDC, strategist, owner } = await deployFixture();

    assert.equal(getAddress(await vaultContract.read.asset()), getAddress(mockUSDC.address), "本位币不匹配");
    assert.equal(getAddress(await vaultContract.read.strategist()), getAddress(strategist.account.address), "策略师地址未正确绑定");
    assert.equal(getAddress(await vaultContract.read.owner()), getAddress(owner.account.address), "金库Owner未正确初始化");
  });

  it("ERC4626 资产存取流:投资者应能正常存入本位币并铸造份额", async function () {
    const { vaultContract, vaultAddress, mockUSDC, investor } = await deployFixture();
    const depositAmount = parseUnits("5000", 6);

    await mockUSDC.write.approve([vaultAddress, depositAmount], { account: investor.account });
    await vaultContract.write.deposit([depositAmount, investor.account.address], { account: investor.account });

    assert.equal(await vaultContract.read.totalAssets(), depositAmount, "金库总资产记录不准");
    assert.equal(await vaultContract.read.balanceOf([investor.account.address]), depositAmount, "份额不匹配");
  });

  it("多资产资产净值(NAV)计算:调仓产生多资产持仓后,预言机应能准确加权并归一化折算", async function () {
    const { vaultContract, vaultAddress, mockUSDC, mockWBTC, investor, strategist, owner } = await deployFixture();
    
    const depositAmount = parseUnits("6000", 6);
    await mockUSDC.write.approve([vaultAddress, depositAmount], { account: investor.account });
    await vaultContract.write.deposit([depositAmount, investor.account.address], { account: investor.account });

    const btcTradeAmount = parseUnits("0.1", 8); 
    
    const mockSwapData = encodeFunctionData({
      abi: mockWBTC.abi,
      functionName: "transfer",
      args: [vaultAddress, btcTradeAmount]
    });
    
    await mockWBTC.write.transfer([vaultAddress, btcTradeAmount], { account: owner.account });
    
    const vaultUsdcBalance = await mockUSDC.read.balanceOf([vaultAddress]);
    const mockBurnData = encodeFunctionData({
      abi: mockUSDC.abi,
      functionName: "transfer",
      args: [owner.account.address, vaultUsdcBalance]
    });

    const minExpectedBalances = [0n, btcTradeAmount];

    await vaultContract.write.executeStrategy(
      [[mockWBTC.address, mockUSDC.address], [mockSwapData, mockBurnData], minExpectedBalances], 
      { account: strategist.account }
    );

    const expectedNAV = parseUnits("6000", 6);
    assert.equal(await vaultContract.read.totalAssets(), expectedNAV, "Chainlink 多资产阶梯换算失败");
  });

  // 💡 新增测试用例:自动化提取收益费(Performance Fee)验证流程
  it("收益费流:金库应能正确调整表现费率,并在调仓获利后由管理员将留存手续费提取至国库", async function () {
    const { vaultContract, vaultAddress, mockWBTC, owner, strategist, treasury, maliciousUser } = await deployFixture();
    
    // 1. 设置表现费率(例如 500 基点 = 5%)
    const feeBps = 500n;
    await vaultContract.write.setPerformanceFee([feeBps], { account: owner.account });
    assert.equal(await vaultContract.read.performanceFeeBps(), feeBps, "表现费率未成功同步");

    // 2. 模拟金库赚取收益(外部流动性池向金库注入 0.5 个 WBTC 作为策略收益盈利)
    const profitAmount = parseUnits("0.5", 8);
    await mockWBTC.write.transfer([vaultAddress, profitAmount], { account: owner.account });

    // 验证此时金库内确实存在这笔等待结算清空的资产
    const btcBalanceInVault = await mockWBTC.read.balanceOf([vaultAddress]);
    assert.equal(btcBalanceInVault, profitAmount, "金库未成功接收策略盈利资产");

    // 3. 安全边界拦截:非 Owner 尝试调用 withdrawFees 应被强制拦截拒绝
    await assert.rejects(
      async () => {
        await vaultContract.write.withdrawFees([mockWBTC.address, treasury.account.address], {
          account: maliciousUser.account
        });
      },
      /OwnableUnauthorizedAccount/, // OpenZeppelin v5 标准的 Ownable 越权错误
      "非所有者不应被允许提取协议表现费"
    );

    // 4. 自动化触发:金库 Owner(或自动化的智能国库脚本)安全将收益代币提取至国库(treasury)
    await vaultContract.write.withdrawFees([mockWBTC.address, treasury.account.address], { 
      account: owner.account 
    });

    // 5. 最终状态断言核验
    const vaultPostBalance = await mockWBTC.read.balanceOf([vaultAddress]);
    const treasuryPostBalance = await mockWBTC.read.balanceOf([treasury.account.address]);

    assert.equal(vaultPostBalance, 0n, "提取后金库内的留存资产手续费应当清零");
    assert.equal(treasuryPostBalance, profitAmount, "国库接收到的表现费数量不匹配");
  });

  it("安全风控拦截:防范超时陈旧预言机喂价(Stale Price)", async function () {
    const { vaultContract, vaultAddress, mockWBTC, wbtcFeed, owner } = await deployFixture();

    const btcAmount = parseUnits("0.05", 8);
    await mockWBTC.write.transfer([vaultAddress, btcAmount], { account: owner.account });

    const tenDaysAgo = BigInt(Math.floor(Date.now() / 1000) - 86400 * 10);
    await wbtcFeed.write.setMockRoundData([
      1n,           
      6000000000000n, 
      tenDaysAgo,   
      tenDaysAgo    
    ], { account: owner.account });

    await assert.rejects(
      async () => {
        await vaultContract.read.totalAssets();
      },
      /StalePriceFeed/,
      "当外部预言机喂价滞后时,金库系统必须强制触发断言熔断"
    );
  });

  it("权限拦截:非策略师或Owner无法调用 executeStrategy 核心资产操作入口", async function () {
    const { vaultContract, maliciousUser } = await deployFixture();

    await assert.rejects(
      async () => {
        await vaultContract.write.executeStrategy([[], [], []], {
          account: maliciousUser.account,
        });
      },
      /UnauthorizedStrategist/,
      "任意非授权账户无权挪动金库内的底层资金"
    );
  });
});


五、 生产环境的最佳安全实践

在将该多资产智能金库部署到生产网(如 Base 或 Monad)之前,仍需在 CI/CD 流水线中补充两项防御墙:

  1. 防范 ERC4626 通胀攻击(Inflation Attack / Donation Attack)

    • 原理:第一位存款人(totalSupply == 0)存入 1 聪(Wei)资产,随后通过直接向金库合约 transfer() 捐赠 10,000 个代币。这会导致每一个 Share 的价格变得极其昂贵,后面的存款人由于四舍五入(Round Down)的存在,铸造出的份额会被无情强制归零,从而导致本金全额被首位攻击者吞噬。
    • 升级对策:利用 OpenZeppelin v5 内置的虚拟流动性(Virtual Shares / Virtual Assets) 机制。在首位存款人注入资金时,合约在底层逻辑上强行给零地址铸造一部分份额(如 (10^{3}) 聪),从物理上打破黑客的资金比例杠杆。
  2. 多预言机冗余切换(Oracle Redundancy)

    • 原理:当 Chainlink 的某些长尾资产喂价因极端市场波动触发暂停熔断时,latestRoundData 会持续 revert,导致金库的存款与提现逻辑全面锁死。
    • 升级对策:在 _getAssetPrice 的内部捕获块中引入二级冗余源(如 API3 或 Pyth Network)。若主预言机报错,则迅速降级采用二级预言机并加以安全滑点溢价,保障金库的充提业务不间断。

六、 结语

DeFAI 的真正壁垒不在于 AI 模型的参数大小,而在于链上执行环境的安全性与确定性。通过将 OpenZeppelin v5 的高度模块化与 Chainlink 的公允价格流相咬合,我们构建了一个无须信任(Trustless)的多资产金库。这一基础设施能够让 AI 智能体在线上纵横驰骋、捕捉 Alpha 的同时,将用户的资金锁在最安全的数学逻辑与断言铁笼之中。