React Native DApp 开发全栈实战·从 0 到 1 系列(兑换-合约部分)
2025年9月15日 21:25
前言
本文借助 Solidity 0.8、OpenZeppelin 与 Chainlink 喂价,构建一套 链上即时汇率结算、链下可信价格驱动 的微型兑换系统。本文将带你完成:
- 部署可铸造 ERC-20(BoykayuriToken,BTK)
- 部署 Chainlink 风格喂价合约(MockV3Aggregator),本地即可模拟 ETH/USD = 2000 的实时价格
- 部署 SwapToken —— 接收 ETH、按市价折算 USD、并立即向用户发放等值 BTK
- 使用 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 风格的流动性池,实现双向兑换