前言本文借助Solidity0.8、OpenZeppelin与Chainlink喂价,构建一套链上即时汇率结算、链下可信价格驱动的微型兑换系统。本文将带你完成:部署可铸造ERC-20(BoykayuriToken,BTK)部署Chainlink风格喂价合约(MockV3
本文借助 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 风格的流动性池,实现双向兑换
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!