React Native DApp 开发全栈实战·从 0 到 1 系列(兑换-合约部分)

  • 木西
  • 发布于 2小时前
  • 阅读 35

前言本文借助Solidity0.8、OpenZeppelin与Chainlink喂价,构建一套链上即时汇率结算、链下可信价格驱动的微型兑换系统。本文将带你完成:部署可铸造ERC-20(BoykayuriToken,BTK)部署Chainlink风格喂价合约(MockV3

前言

本文借助 Solidity 0.8、OpenZeppelin 与 Chainlink 喂价,构建一套 链上即时汇率结算、链下可信价格驱动 的微型兑换系统。本文将带你完成:

  1. 部署可铸造 ERC-20(BoykayuriToken,BTK)
  2. 部署 Chainlink 风格喂价合约(MockV3Aggregator),本地即可模拟 ETH/USD = 2000 的实时价格
  3. 部署 SwapToken —— 接收 ETH、按市价折算 USD、并立即向用户发放等值 BTK
  4. 使用 Hardhat 本地网络 + hardhat-deploy 插件一键启动,5 条指令完成编译、测试、部署全流程 无需前端,无需真实 LINK,即可体验 "价格输入 → 汇率计算 → 代币闪兑" 的完整闭环,为后续接入主网喂价、多币种池子、流动性挖矿奠定可复用的脚手架。

    智能合约

    代币合约

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

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol";

contract SwapToken is Ownable { AggregatorV3Interface internal priceFeed; IERC20 public token; uint public constant TOKENS_PER_USD = 1000; // 1 USD = 1000 MTK

constructor(address _priceFeed, address _token) Ownable(msg.sender) {
    priceFeed = AggregatorV3Interface(_priceFeed);
    token = IERC20(_token);
}

function swap() public payable {
    uint usd = getEthInUsd(msg.value);
    uint amount = usd * TOKENS_PER_USD;
    require(token.balanceOf(address(this)) >= amount, "Not enough liquidity");
    token.transfer(msg.sender, amount);
}

function getEthInUsd(uint ethAmount) public view returns (uint) {
    (, int price, , , ) = priceFeed.latestRoundData(); // price in 8 decimals
    uint ethUsd = (ethAmount * uint(price)) / 1e18; // ETH amount in USD (8 decimals)
    return ethUsd / 1e8; // return USD amount
}

receive() external payable {}

}

#### 喂价合约

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

import { AggregatorV3Interface } from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

contract MockV3Aggregator is AggregatorV3Interface { uint256 public constant versionvar = 4;

uint8 public decimalsvar;
int256 public latestAnswer;
uint256 public latestTimestamp;
uint256 public latestRound;
mapping(uint256 => int256) public getAnswer;
mapping(uint256 => uint256) public getTimestamp;
mapping(uint256 => uint256) private getStartedAt;
string private descriptionvar;

constructor(
    uint8 _decimals,
    string memory _description,
    int256 _initialAnswer
) {
    decimalsvar = _decimals;
    descriptionvar = _description;
    updateAnswer(_initialAnswer);
}

function updateAnswer(int256 _answer) public {
    latestAnswer = _answer;
    latestTimestamp = block.timestamp;
    latestRound++;
    getAnswer[latestRound] = _answer;
    getTimestamp[latestRound] = block.timestamp;
    getStartedAt[latestRound] = block.timestamp;
}

function getRoundData(uint80 _roundId)
    external
    view
    override
    returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    )
{
    return (
        _roundId,
        getAnswer[_roundId],
        getStartedAt[_roundId],
        getTimestamp[_roundId],
        _roundId
    );
}

function latestRoundData()
    external
    view
    override
    returns (
        uint80 roundId,
        int256 answer,
        uint256 startedAt,
        uint256 updatedAt,
        uint80 answeredInRound
    )
{
    return (
        uint80(latestRound),
        latestAnswer,
        getStartedAt[latestRound],
        latestTimestamp,
        uint80(latestRound)
    );
}

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

function description() external view override returns (string memory) {
    return descriptionvar;
}

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

}

#### 兑换合约

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

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol";

contract SwapToken is Ownable { AggregatorV3Interface internal priceFeed; IERC20 public token; uint public constant TOKENS_PER_USD = 1000; // 1 USD = 1000 MTK

constructor(address _priceFeed, address _token) Ownable(msg.sender) {
    priceFeed = AggregatorV3Interface(_priceFeed);
    token = IERC20(_token);
}

function swap() public payable {
    uint usd = getEthInUsd(msg.value);
    uint amount = usd * TOKENS_PER_USD;
    require(token.balanceOf(address(this)) >= amount, "Not enough liquidity");
    token.transfer(msg.sender, amount);
}

function getEthInUsd(uint ethAmount) public view returns (uint) {
    (, int price, , , ) = priceFeed.latestRoundData(); // price in 8 decimals
    uint ethUsd = (ethAmount * uint(price)) / 1e18; // ETH amount in USD (8 decimals)
    return ethUsd / 1e8; // return USD amount
}

receive() external payable {}

}

#### 编译指令:npx hardhat compile
# 测试合约

const { ethers } = require("hardhat"); const { expect } = require("chai");

describe("SwapToken", function () { let SwapToken, MockToken, MockV3Aggregator; let owner, user;

beforeEach(async () => { [owner, user] = await ethers.getSigners(); await deployments.fixture(["MockV3Aggregator","token","SwapToken"]); const MockTokenAddress = await deployments.get("MyToken"); // 存入资产 // 奖励代币(USDC) const MockV3AggregatorAddress = await deployments.get("MockV3Aggregator"); const SwapTokenAddress = await deployments.get("SwapToken");

    MockToken = await ethers.getContractAt("MyToken", MockTokenAddress.address);
    MockV3Aggregator = await ethers.getContractAt("MockV3Aggregator", MockV3AggregatorAddress.address);
    SwapToken = await ethers.getContractAt("SwapToken", SwapTokenAddress.address);
    // 给SwapToken合约铸造资产
    await MockToken.mint(await SwapToken.getAddress(), ethers.parseEther("1000000"));
    console.log('name',await MockToken.name())
    console.log("symbol",await MockToken.symbol())
    console.log(await MockV3Aggregator.latestAnswer())

});

it("Should swap ETH for MTK", async function () { const ethAmount = ethers.parseEther("1"); // 1 ETH = 2000 USD = 2,000,000 MTK await SwapToken.connect(user).swap({ value: ethAmount });

const balance = await MockToken.balanceOf(user.address);
console.log(balance)
expect(balance).to.equal(2000 * 1000); // 2,000,000 MTK

}); });

#### 测试指令:npx hardhat test ./test/xxxx.js
# 部署合约

module.exports = async ({getNamedAccounts,deployments})=>{ const getNamedAccount = (await getNamedAccounts()).firstAccount; const secondAccount= (await getNamedAccounts()).secondAccount; console.log('secondAccount',secondAccount) const {deploy,log} = deployments; const MyAsset = await deployments.get("MyToken"); //执行MockV3Aggregator部署合约 const MockV3Aggregator=await deploy("MockV3Aggregator",{ from:getNamedAccount, args: [8,"ETH/USDC", 200000000000],//参数 log: true, }) console.log("MockV3Aggregator合约地址:", MockV3Aggregator.address); const SwapToken=await deploy("SwapToken",{ from:getNamedAccount, args: [MockV3Aggregator.address,MyAsset.address],//参数 喂价,资产地址 log: true, }) // await hre.run("verify:verify", { // address: TokenC.address, // constructorArguments: [TokenName, TokenSymbol], // }); console.log('SwapToken 兑换合约地址',SwapToken.address) } module.exports.tags = ["all", "SwapToken"];


#### 部署指令:npx hardhat deploy --tags token,MockV3Aggregator,SwapToken
# 总结
通过本文,我们完成了:

-   **价格层**:MockV3Aggregator 遵循 Chainlink 接口,可无缝替换为主网喂价
-   **资产层**:ERC-20 代币自带 `mint` 与 `Ownable`,方便快速补充流动性
-   **兑换层**:SwapToken 利用 `latestRoundData()` 实时计算 ETH→USD→BTK 数量,全程链上可查
-   **脚本层**:Hardhat 脚本化部署 + 测试固件,保证"一键重置、秒级回滚",让迭代安全又高效

后续优化: 
-   把 Mock 喂价替换为 ETH/USD 主网聚合器
-   引入 Uniswap V2 风格的流动性池,实现双向兑换
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。