目标:在不改变资金池中对标币种(如 USDC/ETH/USDT)数量的前提下,以固定时间间隔从 AMM 资金池合约地址中单边燃烧本代币的一定比例,并在燃烧后对交易对执行 sync() 以刷新储备。
⚠️ 风险与注意事项(生产环境必读)
价格与套利:单边燃烧会直接减少池内本币余额、价格瞬时上升,套利者会用对标币买回本币直到价格回归均衡。其经济效果近似于自动回购并销毁,但用的是池内存量而非增量资金。
合约权限:该方案需要代币合约能从任意地址(包括 LP 合约地址)移动本币余额(即 _transfer(from, to, amount) 的 from 可自由指定)。这是常见于税费代币/黑名单代币的设计,具有主观性与可监管性,务必开启多签/时间锁与上限约束。
协议兼容:燃烧后务必调用 pair.sync(),否则储备与余额不一致,可能干扰路由报价/滑点判断。
上限与节流:请设置最大单次燃烧比例(如 ≤1%)与最小间隔(如 ≥1 小时),并在合约中强制执行。
可关闭与紧急开关:提供 pause()/unpause(),以及调整参数的治理流程。
一、功能概览
-
固定时间间隔执行单边燃烧(仅销毁本代币,保持对标币不动)。
-
自动计算当前 LP 合约地址(可固定或允许治理更新一次)。
-
单次燃烧比例以 BPS(基点) 表示并设定上限。
-
燃烧后对 IUniswapV2Pair 调用 sync()。
-
可手动/自动(Keeper/Bot)触发执行;任何人可调用但受间隔约束。
二、目录
-
合约实现
接口与常量
核心代币合约 SingleSidedBurnToken
最小化 Pair 接口
-
测试工程(Foundry)
Mocks:对标币 & 最小化 PairMock
Foundry 测试用例
运行步骤
-
运维建议与生产参数
-
常见问题
三、合约实现
Solidity ^0.8.24,依赖 OpenZeppelin(ERC20、Ownable、Pausable、ReentrancyGuard)。
接口与常量
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IUniswapV2PairMinimal {
function sync() external;
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function token0() external view returns (address);
function token1() external view returns (address);
}
核心代币合约 SingleSidedBurnToken
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface IUniswapV2PairMinimal {
function sync() external;
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
function token0() external view returns (address);
function token1() external view returns (address);
}
/// @title Single-Sided LP Burn Token
/// @notice 在固定间隔内,从 LP 合约地址单边燃烧本代币,并调用 pair.sync() 刷新储备。
contract SingleSidedBurnToken is ERC20, Ownable2Step, Pausable, ReentrancyGuard {
/// @dev 死亡地址(常用 0x000...dEaD)
address public constant DEAD = 0x000000000000000000000000000000000000dEaD;
/// @dev 交易对地址(UniswapV2Pair 或兼容)
address public pair;
/// @dev 最小燃烧间隔(秒)
uint256 public burnInterval;
/// @dev 单次燃烧比例(基点,10000 = 100%)
uint256 public burnBps;
/// @dev 单次燃烧比例上限(安全阈值,默认 100 = 1%)
uint256 public immutable maxBurnBps;
/// @dev 上次燃烧时间戳
uint256 public lastBurnAt;
/// @dev 是否已设置过 pair(仅允许设置一次,避免治理误操作)
bool public pairFinalized;
event PairSet(address indexed pair, bool finalized);
event BurnExecuted(uint256 amount, uint256 timestamp);
event ParamsUpdated(uint256 burnInterval, uint256 burnBps);
error PairNotSet();
error PairAlreadyFinalized();
error NotDue();
error BurnTooHigh();
constructor(
}
实现要点
-
合约内部直接调用 _transfer(pair, DEAD, amount),因此无需授权即可从 LP 地址移动本币余额(这也是本方案的核心前提)。
-
燃烧后调用 pair.sync(),令 AMM 储备与真实余额一致(对标币数量不变)。
-
通过 burnInterval、burnBps、maxBurnBps 做强约束,配合 pause() 实现紧急开关。
最小化 Pair 接口
上文已给出 IUniswapV2PairMinimal,在生产部署中请直接使用真实的 IUniswapV2Pair 接口地址。
四、测试工程(Foundry)
使用最小化的 PairMock 来模拟 sync() 与储备行为,便于快速本地验证单边燃烧逻辑。生产部署时请对接真实 AMM(UniswapV2/PancakeV2 等)并在测试网做集成测试。
Mocks:对标币 & 最小化 PairMock
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {MockERC20} from "./MockERC20.sol";
/// @title PairMock
/// @notice 最小化的 V2 Pair,用于测试 `sync()` 刷新储备。
contract PairMock {
address public token0;
address public token1;
uint112 private _reserve0;
uint112 private _reserve1;
uint32 private _blockTimestampLast;
constructor(address _t0, address _t1) {
require(_t0 != _t1, "identical");
(token0, token1) = _t0 < _t1 ? (_t0, _t1) : (_t1, _t0);
}
function getReserves() external view returns (uint112, uint112, uint32) {
return (_reserve0, _reserve1, _blockTimestampLast);
}
/// @notice 简化的添加流动性:直接把当前余额作为储备
function sync() public {
_updateReserves();
}
function _updateReserves() internal {
uint256 bal0 = ERC20(token0).balanceOf(address(this));
uint256 bal1 = ERC20(token1).balanceOf(address(this));
require(bal0 <= type(uint112).max && bal1 <= type(uint112).max, "overflow");
_reserve0 = uint112(bal0);
_reserve1 = uint112(bal1);
_blockTimestampLast = uint32(block.timestamp);
}
}
Foundry 测试用例
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import {SingleSidedBurnToken} from "../contracts/SingleSidedBurnToken.sol";
import {MockERC20} from "../contracts/mocks/MockERC20.sol";
import {PairMock} from "../contracts/mocks/PairMock.sol";
interface IPairLike { function sync() external; function getReserves() external view returns (uint112,uint112,uint32); function token0() external view returns (address); function token1() external view returns (address); }
contract SingleSidedBurnTest is Test {
SingleSidedBurnToken token; // 被测代币
MockERC20 usdc; // 对标币
PairMock pair; // 简化版交易对
address owner = address(0xABCD);
function setUp() public {
vm.startPrank(owner);
// 部署对标币 USDC(6位)
usdc = new MockERC20("USD Coin", "USDC", 6);
// 部署被测代币,初始 1e9 * 1e18
token = new SingleSidedBurnToken(
"BurnableLP",
"BLP",
1_000_000_000 ether,
1 hours, // burnInterval
100, // burnBps = 1%
100 // maxBurnBps = 1%
);
// 部署 Pair
pair = new PairMock(address(token), address(usdc));
// 铸币给 owner
usdc.mint(owner, 10_000_000 * 1e6);
// 向 Pair 添加初始流动性:10,000,000 BLP : 1,000,000 USDC
token.transfer(address(pair), 10_000_000 ether);
usdc.transfer(address(pair), 1_000_000 * 1e6);
pair.sync();
// 设定代币中的 pair 地址并锁定
token.setPair(address(pair), true);
vm.stopPrank();
}
function test_SingleSidedBurn_DoesNotChangeQuoteReserves() public {
// 读取初始储备
(uint112 r0_before, uint112 r1_before,) = pair.getReserves();
// 推进时间到可燃烧
vm.warp(block.timestamp + 1 hours + 1);
// 任何人都可调用
token.performLiquiditySingleSidedBurn();
// 读取燃烧后的储备
(uint112 r0_after, uint112 r1_after,) = pair.getReserves();
// 判断哪个是本币储备
address t0 = pair.token0();
assertApproxEqAbs(base_after, expected, 1, "base reserve decreased by bps");
我把完整的实现、项目结构和 Foundry 测试都整理到右侧画布里了,包括:
-
生产可用的 SingleSidedBurnToken 合约(基于 OpenZeppelin)
-
单边燃烧逻辑(从 LP 地址直接把本币转到 0x…dEaD,并 pair.sync() 刷新储备)
-
Pair/USDC 的最小化 mock 与完整的 Foundry 测试
-
foundry.toml、依赖与运行步骤
-
上线前风险点与运维参数建议
❝本公众号发布的内容除特别标明外版权归原作者所有。若涉及版权问题,请联系我们。所有信息及评论区内容仅供参考,请读者自行判断信息真伪,不构成任何投资建议。据此产生的任何损失,本公众号概不负责,亦不负任何法律责任。

