前言本文围绕永续合约展开全面梳理,既涵盖核心理论知识,也包含完整的技术开发实践内容。理论部分系统梳理了永续合约的基础概念、核心运行机制、实际应用场景及风险提示;技术实践部分则基于HardhatV3开发框架,借助OpenZeppelin、Chainlink工具库及Solidity编程语言,从最小
本文围绕永续合约展开全面梳理,既涵盖核心理论知识,也包含完整的技术开发实践内容。理论部分系统梳理了永续合约的基础概念、核心运行机制、实际应用场景及风险提示;技术实践部分则基于Hardhat V3开发框架,借助OpenZeppelin、Chainlink工具库及Solidity编程语言,从最小单位实现永续合约的开发、测试到部署的全流程。
1. 是什么:永续合约的定义
永续合约(Perpetual Contract)是一种特殊的加密货币期货合约,最大的特点是没有到期日或结算日。
传统期货 vs. 永续合约:
核心目的: 让交易者能够长期持有某个方向的头寸(做多或做空),而不必担心合约到期被迫换仓。
永续合约之所以能 “永续” 存在且不偏离现货价格,依赖于两个核心机制:资金费率(Funding Rate)和标记价格(Mark Price) 。
这是永续合约最独特的地方。由于没有到期日,为了防止合约价格严重偏离现货价格(BTC/USD),交易所引入了资金费率。
作用: 强制将合约价格拉回现货价格水平。
谁付给谁?
支付频率: 通常每 8 小时一次(UTC 时间 00:00, 08:00, 16:00)。
为了防止市场剧烈波动时的 “插针” 爆仓,永续合约不直接使用最新成交价作为强平依据,而是使用标记价格。
永续合约允许用户借贷资金进行交易。
永续合约主要服务于投机、套利和对冲这三类需求。
这是最常见的场景。交易者看好或看衰某种加密货币的短期走势。
矿工或大额持仓者用于规避现货下跌风险。
利用资金费率的周期性进行低风险获利。
正向套利(吃费率): 当资金费率很高(例如 + 0.1%,每 8 小时),说明多头在向空头付费。
期现套利: 利用合约价格与现货价格的价差进行搬砖(虽然资金费率机制已经大大压缩了这种价差的空间)。
一句话总结:永续合约是加密货币市场的 “涡轮增压器”,它通过资金费率这一巧妙的机制,让带杠杆的合约交易像现货一样永续存在,既满足了交易者以小博大的投机需求,也提供了做空和对冲的金融工具。
⚠️ 风险提示:虽然机制精妙,但永续合约是高风险产品。
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.24;import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract BoykaYuriToken is ERC20, ERC20Burnable, Ownable, ERC20Permit { constructor(address recipient, address initialOwner) ERC20("MyToken", "MTK") Ownable(initialOwner) ERC20Permit("MyToken") { _mint(recipient, 1000000 * 10 ** decimals()); } function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } }
### 喂价合约
**特殊说明**:需要说明的是,若合约部署在主链或测试链,可直接在Chainlink上查看合约地址及实例;而为便于本地测试,建议另行部署Mock喂价合约。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
/**
@dev 用于本地测试环境模拟 Chainlink 预言机 */ contract MockV3Aggregator is AggregatorV3Interface { int256 private _answer; uint8 public immutable decimals;
constructor(int256 initialAnswer, uint8 _decimals) { _answer = initialAnswer; decimals = _decimals; }
function latestRoundData() external view override returns ( uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound ) { return (1, _answer, block.timestamp, block.timestamp, 1); }
// 辅助函数:更新模拟价格 function updateAnswer(int256 newAnswer) external { _answer = newAnswer; }
// // 实现接口所需的所有其他视图函数(虽然在测试中可能用不到) // function description() external view override returns (string memory) { return "Mock ETH/USD"; } // function version() external view override returns (uint256) { return 3; } // function getRoundData(uint80) external view override returns (uint80, int256, uint256, uint256, uint80) { // revert("Not implemented"); // } // 修改后:通过读取 decimals 让编译器保持 view 属性 function description() external view override returns (string memory) { decimals; // 虚拟读取 return "Mock ETH/USD"; }
function version() external view override returns (uint256) { decimals; // 虚拟读取 return 3; }
function getRoundData(uint80) external view override returns (uint80, int256, uint256, uint256, uint80) { decimals; // 虚拟读取 revert("Not implemented"); } }
### 永续合约
// SPDX-License-Identifier: MIT pragma solidity ^0.8.27;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
/**
@dev 优化的永续合约 Demo,包含精度修复和 Gas 优化 */ contract PerpTrade is ReentrancyGuard, AccessControl { using SafeERC20 for IERC20;
bytes32 public constant LIQUIDATOR_ROLE = keccak256("LIQUIDATOR_ROLE");
uint256 public constant BPS_DENOMINATOR = 10000; // 基点分母 uint256 public constant PRECISION = 1e18; uint256 public constant MAX_LEVERAGE = 20 * PRECISION; uint256 public constant MAINTENANCE_MARGIN_BPS = 500; // 5% 维持保证金率
IERC20 public immutable collateralToken; AggregatorV3Interface public immutable priceFeed; uint8 public immutable priceDecimals;
struct Position { uint128 size; // 名义价值 (1e18) uint128 collateral; // 保证金数量 (1e18) uint64 entryPrice; // 喂价原始精度 uint64 lastUpdate; // 时间戳 bool isLong; bool isActive; }
mapping(address => Position[]) public positions;
event Opened(address indexed user, uint256 pid, bool isLong, uint256 size, uint256 collateral, uint256 price); event Closed(address indexed user, uint256 pid, uint256 exitPrice, int256 pnl); event Liquidated(address indexed user, uint256 pid, address liquidator, uint256 price, uint256 reward);
constructor(address _collateral, address _priceFeed) { collateralToken = IERC20(_collateral); priceFeed = AggregatorV3Interface(_priceFeed); priceDecimals = priceFeed.decimals(); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); _grantRole(LIQUIDATOR_ROLE, msg.sender); }
/**
/**
@param leverage 杠杆倍数 (1e18 为 1x) */ function open(bool isLong, uint256 collateralAmount, uint256 leverage) external nonReentrant { require(leverage >= PRECISION && leverage <= MAX_LEVERAGE, "Invalid Leverage"); require(collateralAmount > 0, "Zero Collateral");
// 转移保证金到合约 collateralToken.safeTransferFrom(msg.sender, address(this), collateralAmount);
uint256 price = getNormalizedPrice(); uint256 size = (collateralAmount * leverage) / PRECISION;
positions[msg.sender].push(Position({ size: uint128(size), collateral: uint128(collateralAmount), entryPrice: uint64(price / (10**(18 - priceDecimals))), // 存储原始精度节省空间 lastUpdate: uint64(block.timestamp), isLong: isLong, isActive: true }));
emit Opened(msg.sender, positions[msg.sender].length - 1, isLong, size, collateralAmount, price); }
/**
/**
@notice 内部平仓逻辑 */ function _close(address user, uint256 pid, Position storage p) internal { uint256 currentPrice = getNormalizedPrice(); (bool profit, uint256 pnlValue) = _calculatePnL(p, currentPrice);
uint256 payOut; if (profit) { payOut = p.collateral + pnlValue; } else { payOut = pnlValue >= p.collateral ? 0 : p.collateral - pnlValue; }
p.isActive = false; if (payOut > 0) collateralToken.safeTransfer(user, payOut);
emit Closed(user, pid, currentPrice, profit ? int256(pnlValue) : -int256(pnlValue)); }
/**
@notice 清算逻辑 */ function liquidate(address user, uint256 pid) external nonReentrant onlyRole(LIQUIDATOR_ROLE) { Position storage p = positions[user][pid]; require(p.isActive, "Not Active");
uint256 currentPrice = getNormalizedPrice(); (bool profit, uint256 pnlValue) = _calculatePnL(p, currentPrice);
// 清算条件:非盈利且亏损导致剩余保证金低于维持保证金要求 bool isLiquidatable = !profit && (p.collateral <= pnlValue || (p.collateral - pnlValue) BPS_DENOMINATOR < uint256(p.size) MAINTENANCE_MARGIN_BPS / 100);
require(isLiquidatable, "Position Safe");
uint256 reward = uint256(p.collateral) / 20; // 5% 清算奖励 p.isActive = false;
collateralToken.safeTransfer(msg.sender, reward); // 剩余保证金通常留给协议作为风险基金
emit Liquidated(user, pid, msg.sender, currentPrice, reward); }
/**
@dev 计算 PnL,返回 (是否盈利, 盈亏金额) / function _calculatePnL(Position memory p, uint256 currentPrice) internal view returns (bool profit, uint256 pnlValue) { uint256 entryPriceStandard = uint256(p.entryPrice) (10**(18 - priceDecimals));
if (currentPrice == entryPriceStandard) return (true, 0);
uint256 priceDiff; if (p.isLong) { profit = currentPrice > entryPriceStandard; priceDiff = profit ? currentPrice - entryPriceStandard : entryPriceStandard - currentPrice; } else { profit = currentPrice < entryPriceStandard; priceDiff = profit ? entryPriceStandard - currentPrice : currentPrice - entryPriceStandard; }
pnlValue = (uint256(p.size) * priceDiff) / entryPriceStandard; }
function positionCount(address user) external view returns (uint256) { return positions[user].length; } }
## **测试脚本**
**测试说明**:
* **验证初始化参数与标准化价格**
* **用户开仓 (Long 10x) 应该成功**
* **价格上涨后平仓应该获利**
* **爆仓逻辑验证 (Liquidate)**
import assert from "node:assert/strict"; import { describe, it, beforeEach } from "node:test"; import hre from "hardhat"; import { parseEther, parseUnits, formatUnits } from "viem";
describe("PerpTrade 永续合约逻辑测试", async function () { // 获取 Hardhat Viem 实例 const { viem } = await hre.network.connect();
let owner: any, user: any;
let publicClient: any;
let PerpTrade: any;
let MockPriceFeed: any;
let CollateralToken: any;
const INITIAL_PRICE = "3500.65"; // $3500.65
const PRICE_DECIMALS = 8;
beforeEach(async function () {
publicClient = await viem.getPublicClient();
[owner, user] = await viem.getWalletClients();
// 1. 部署代币 (假设精度为 18)
CollateralToken = await viem.deployContract("BoykaYuriToken", [
owner.account.address,
owner.account.address
]);
// 2. 部署模拟预言机 (价格, 精度)
const initialPriceWei = parseUnits(INITIAL_PRICE, PRICE_DECIMALS);
MockPriceFeed = await viem.deployContract("MockV3Aggregator", [
initialPriceWei,
PRICE_DECIMALS
]);
// 3. 部署永续合约
PerpTrade = await viem.deployContract("PerpTrade", [
CollateralToken.address,
MockPriceFeed.address
]);
// 4. 给用户准备点钱并授权
const fundAmount = parseEther("10000");
await CollateralToken.write.transfer([user.account.address, fundAmount]);
// 用户需要授权给 PerpTrade 才能开仓
await CollateralToken.write.approve([PerpTrade.address, fundAmount], {
account: user.account
});
// --- 新增:给 PerpTrade 合约注入“利润储备金” ---
// 确保合约有足够的余额支付给获利的交易者
const treasuryAmount = parseEther("50000");
await CollateralToken.write.transfer([PerpTrade.address, treasuryAmount]);
});
it("验证初始化参数与标准化价格", async function () {
const price = await PerpTrade.read.getNormalizedPrice();
// 应该被标准化为 1e18
assert.equal(price, parseEther(INITIAL_PRICE));
const tokenAddress = await PerpTrade.read.collateralToken();
assert.equal(tokenAddress.toLowerCase(), CollateralToken.address.toLowerCase());
});
it("用户开仓 (Long 10x) 应该成功", async function () {
const collateral = parseEther("100"); // 100 保证金
const leverage = parseEther("10"); // 10x 杠杆
// 执行开仓
await PerpTrade.write.open([true, collateral, leverage], {
account: user.account
});
const count = await PerpTrade.read.positionCount([user.account.address]);
assert.equal(count, 1n);
const position = await PerpTrade.read.positions([user.account.address, 0n]);
// [size, collateral, entryPrice, lastUpdate, isLong, isActive]
assert.equal(position[0], parseEther("1000")); // size = 100 * 10
assert.equal(position[1], collateral);
assert.equal(position[4], true); // isLong
assert.equal(position[5], true); // isActive
});
it("价格上涨后平仓应该获利", async function () {
const collateral = parseEther("100");
const leverage = parseEther("10"); // 1000 USD size
await PerpTrade.write.open([true, collateral, leverage], { account: user.account });
// 模拟价格上涨 10%: 3500.65 -> 3850.715
const newPrice = parseUnits("3850.715", PRICE_DECIMALS);
await MockPriceFeed.write.updateAnswer([newPrice]);
// 获取用户平仓前的余额
const balBefore = await CollateralToken.read.balanceOf([user.account.address]);
// 平仓
await PerpTrade.write.close([0n], { account: user.account });
const balAfter = await CollateralToken.read.balanceOf([user.account.address]);
// 盈利计算: size(1000) * delta(10%) = 100
// 总返还 = 100(保证金) + 100(盈利) = 200
assert.ok(balAfter > balBefore, "余额应该增加");
const profit = balAfter - balBefore;
// 允许微小的精度舍入误差
assert.ok(profit >= parseEther("199.9") && profit <= parseEther("200.1"));
});
it("爆仓逻辑验证 (Liquidate)", async function () {
const collateral = parseEther("100");
const leverage = parseEther("20"); // 20x 杠杆极易爆仓
await PerpTrade.write.open([true, collateral, leverage], { account: user.account });
// 价格下跌 5%, 亏损即达到 100% 保证金 (20x * 5% = 100%)
const crashPrice = parseUnits("3325.6175", PRICE_DECIMALS);
await MockPriceFeed.write.updateAnswer([crashPrice]);
// 管理员(Owner)执行清算
await PerpTrade.write.liquidate([user.account.address, 0n], {
account: owner.account
});
const position = await PerpTrade.read.positions([user.account.address, 0n]);
assert.equal(position[5], false, "仓位应该已关闭");
// 检查管理员是否拿到了清算奖励 (5%)
const adminReward = await CollateralToken.read.balanceOf([owner.account.address]);
assert.ok(adminReward > 0n);
});
});
## **部署脚本**
// scripts/deploy.js import { network, artifacts } from "hardhat"; import {parseEther,parseUnits} from "viem" async function main() { // 连接网络 const { viem } = await network.connect({ network: network.name });//指定网络进行链接
// 获取客户端 const [deployer] = await viem.getWalletClients(); const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address; console.log("部署者的地址:", deployerAddress); // 加载合约 const BoykaYuriTokenArtifact = await artifacts.readArtifact("BoykaYuriToken");
// 部署(构造函数参数:recipient, initialOwner) const BoykaYuriTokenHash = await deployer.deployContract({ abi: BoykaYuriTokenArtifact.abi,//获取abi bytecode: BoykaYuriTokenArtifact.bytecode,//硬编码 args: [deployerAddress,deployerAddress],//process.env.RECIPIENT, process.env.OWNER });
// 等待确认并打印地址 const BoykaYuriTokenArtifactReceipt = await publicClient.waitForTransactionReceipt({ hash: BoykaYuriTokenHash }); console.log("BoykaYuriToken合约地址:", BoykaYuriTokenArtifactReceipt.contractAddress); const MockV3AggregatorArtifact = await artifacts.readArtifact("MockV3Aggregator"); // 部署(构造函数参数:recipient, initialOwner) const MOCK_PRICE = 3500.65; const PRICE_DECIMALS = 8; const initialPriceWei = parseUnits(MOCK_PRICE.toString(), PRICE_DECIMALS); const MockV3AggregatorHash = await deployer.deployContract({ abi: MockV3AggregatorArtifact.abi, bytecode: MockV3AggregatorArtifact.bytecode, // 必须匹配构造函数顺序: [initialAnswer, _decimals] args: [initialPriceWei, PRICE_DECIMALS], }); // 等待确认并打印地址 const MockV3AggregatorArtifactReceipt = await publicClient.waitForTransactionReceipt({ hash: MockV3AggregatorHash }); console.log("MockV3Aggregator合约地址:", MockV3AggregatorArtifactReceipt.contractAddress); const PerpTradeArtifact = await artifacts.readArtifact("PerpTrade"); // 部署(构造函数参数:recipient, initialOwner) const PerpTradeHash = await deployer.deployContract({ abi: PerpTradeArtifact.abi,//获取abi bytecode: PerpTradeArtifact.bytecode,//硬编码 args: [BoykaYuriTokenArtifactReceipt.contractAddress,MockV3AggregatorArtifactReceipt.contractAddress],//process.env.RECIPIENT, process.env.OWNER }); // 等待确认并打印地址 const PerpTradeArtifactReceipt = await publicClient.waitForTransactionReceipt({ hash: PerpTradeHash }); console.log("PerpTrade合约地址:", PerpTradeArtifactReceipt.contractAddress);
}
main().catch(console.error);
# 结语
至此,关于永续合约的相关概念知识梳理,以及对应的代码实现工作已全部完整呈现。 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!