You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Truffle测试Chainlink ETH转USD功能遇回滚错误求助

解决Chainlink价格喂料测试回滚错误

问题场景

我编写了集成Chainlink AggregatorV3Interface的Bank.sol合约,实现convertToUSD方法将用户ETH余额转换为USD,但用Truffle测试时,存款功能正常,调用convertToUSD却触发回滚错误:

VM Exception while processing transaction: revert

Bank.sol合约代码

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract Bank is Initializable, AccessControlUpgradeable {
    // Code to handle two different roles: Manager and User
    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
    bytes32 public constant USER_ROLE = keccak256("USER_ROLE");
    mapping(address => uint256) private _balances;


    // Code for initializer
    function initialize() public initializer {

    }

    function deposit() public payable {
        _balances[msg.sender] += msg.value;

    }

    function convertToUSD(address account) public view returns (uint256) {
        uint256 ethAmount = _balances[account];
        int256 price = getLatestPrice();
        uint256 usdAmount = uint256(price) * ethAmount / 1e18;
        return usdAmount;
    }

    function getLatestPrice() public view returns (int256) {
        AggregatorV3Interface priceFeed = AggregatorV3Interface(0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e);
        (, int256 price, , , ) = priceFeed.latestRoundData();
        return price;
    }

    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }

    function withdraw(uint _amount) public {
        require (_balances[msg.sender] >= _amount, "Insufficient balance");
        _balances[msg.sender] -= _amount;
        (bool sent,) = msg.sender.call{value: _amount}("sent");
        require(sent, "Failed to Complete");
    }
}

Truffle测试代码

const Bank = artifacts.require('Bank.sol')

contract("Bank", async accounts => {
    it("User should be able to deposit funds", async () => {
        const bank = await Bank.new()
        const depositer = accounts[1]
        const amount = web3.utils.toWei('10', 'ether')
        await bank.deposit({from: depositer, value: amount})
        let balance = await bank.balanceOf(depositer)
        balance = parseInt(web3.utils.fromWei(balance, 'ether'))
        assert.equal(balance, 10)
        let usd = await bank.convertToUSD(depositer)
        console.log(usd)
    })
})

问题原因

你在getLatestPrice中使用的地址0xD4a33860578De61DBAbDc8BFdb98FD742fA7028e是以太坊主网的ETH/USD价格喂料合约,但Truffle本地测试链(如Ganache)上不存在这个合约,调用不存在的合约会触发回滚。此外,原合约的initialize函数未调用AccessControl的初始化方法,可能导致潜在的存储问题。

解决方案

1. 修改合约支持可配置价格喂料地址

更新Bank.sol,将价格喂料地址改为可配置,并完成AccessControl的初始化:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract Bank is Initializable, AccessControlUpgradeable {
    bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE");
    bytes32 public constant USER_ROLE = keccak256("USER_ROLE");
    mapping(address => uint256) private _balances;
    AggregatorV3Interface public priceFeed;

    function initialize(address _priceFeed) public initializer {
        __AccessControl_init();
        priceFeed = AggregatorV3Interface(_priceFeed);
    }

    function deposit() public payable {
        _balances[msg.sender] += msg.value;
    }

    function convertToUSD(address account) public view returns (uint256) {
        uint256 ethAmount = _balances[account];
        int256 price = getLatestPrice();
        uint256 usdAmount = uint256(price) * ethAmount / 1e18;
        return usdAmount;
    }

    function getLatestPrice() public view returns (int256) {
        (, int256 price, , , ) = priceFeed.latestRoundData();
        return price;
    }

    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }

    function withdraw(uint _amount) public {
        require (_balances[msg.sender] >= _amount, "Insufficient balance");
        _balances[msg.sender] -= _amount;
        (bool sent,) = msg.sender.call{value: _amount}("");
        require(sent, "Failed to Complete");
    }
}

2. 编写模拟价格喂料合约

创建MockAggregator.sol,用于本地测试时返回固定价格:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

contract MockAggregator is AggregatorV3Interface {
    int256 public latestPrice;

    constructor(int256 _price) {
        latestPrice = _price;
    }

    function latestRoundData() external view returns (
        uint80, int256, uint256, uint256, uint80
    ) {
        return (0, latestPrice, 0, 0, 0);
    }

    function decimals() external pure returns (uint8) {
        return 8;
    }

    // 实现其他未使用的接口方法
    function description() external pure returns (string memory) {
        return "Mock ETH/USD Price Feed";
    }

    function version() external pure returns (uint256) {
        return 1;
    }

    function getRoundData(uint80) external view returns (
        uint80, int256, uint256, uint256, uint80
    ) {
        return (0, latestPrice, 0, 0, 0);
    }
}

3. 更新测试代码

修改Truffle测试,先部署模拟喂料合约,再初始化Bank合约:

const Bank = artifacts.require('Bank')
const MockAggregator = artifacts.require('MockAggregator')

contract("Bank", async accounts => {
    it("User should deposit funds and convert balance to USD", async () => {
        // 模拟ETH价格为2000 USD(Chainlink价格喂料返回8位小数,所以是2000 * 1e8 = 200000000000)
        const mockPrice = web3.utils.toBN('200000000000')
        const mockAggregator = await MockAggregator.new(mockPrice)
        
        // 初始化Bank合约,传入模拟喂料地址
        const bank = await Bank.new()
        await bank.initialize(mockAggregator.address)
        
        const depositer = accounts[1]
        const amount = web3.utils.toWei('10', 'ether')
        await bank.deposit({from: depositer, value: amount})
        
        // 验证ETH余额
        let balance = await bank.balanceOf(depositer)
        balance = parseInt(web3.utils.fromWei(balance, 'ether'))
        assert.equal(balance, 10, "Deposit amount mismatch")
        
        // 验证USD转换结果
        let usd = await bank.convertToUSD(depositer)
        // 预期值:10 ETH * 2000 USD/ETH = 20000 USD(结果为20000 * 1e8 = 2000000000000)
        const expectedUsd = web3.utils.toBN('2000000000000')
        assert.equal(usd.toString(), expectedUsd.toString(), "USD conversion mismatch")
        console.log(`USD value: ${web3.utils.fromWei(usd, 'gwei')} USD`)
    })
})

额外注意事项

  • 原合约的withdraw函数中,call的参数"sent"是多余的,会导致发送ETH时附带无用数据,已修改为空字符串。
  • 部署到主网时,只需传入主网的Chainlink价格喂料地址即可正常使用。

内容的提问来源于stack exchange,提问作者Darryl Soh Soon Yong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 13:57:55