Solidity里一个超硬核的主题——安全的代币燃烧!在区块链上,代币燃烧就是把代币永久销毁,减少总供给,可能是为了控通胀、提价值,或者搞个活动吸引眼球。但这事可不是随便写两行代码就完,烧不好分分钟出漏洞,代币飞了,项目也凉凉!代币燃烧的核心概念先搞清楚几个关键点:代币燃烧:将代币从流通中永
Solidity里一个超硬核的主题——安全的代币燃烧!在区块链上,代币燃烧就是把代币永久销毁,减少总供给,可能是为了控通胀、提价值,或者搞个活动吸引眼球。但这事可不是随便写两行代码就完,烧不好分分钟出漏洞,代币飞了,项目也凉凉!
先搞清楚几个关键点:
totalSupply并转移到“黑洞地址”(如0x0)或直接销毁余额。totalSupply和用户余额必须同步。0x0地址只是转移,实际销毁需更新totalSupply。delegatecall或外部调用需防重入。咱们用Solidity 0.8.20,结合OpenZeppelin和Hardhat,从基础燃烧到多签和批量机制,逐步实现安全的代币燃烧。
用Hardhat搭建开发环境,写和测试合约。
mkdir token-burn-demo
cd token-burn-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @openzeppelin/contracts @openzeppelin/contracts-upgradeable
npm install ethers
初始化Hardhat:
npx hardhat init
选择TypeScript项目,安装依赖:
npm install --save-dev ts-node typescript @types/node @types/mocha
目录结构:
token-burn-demo/
├── contracts/
│ ├── BasicBurn.sol
│ ├── ReentrancyProtectedBurn.sol
│ ├── MultiSigBurn.sol
│ ├── BatchBurn.sol
│ ├── BurnWithReward.sol
├── scripts/
│ ├── deploy.ts
├── test/
│ ├── TokenBurn.test.ts
├── hardhat.config.ts
├── tsconfig.json
├── package.json
tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"outDir": "./dist",
"rootDir": "./"
},
"include": ["hardhat.config.ts", "scripts", "test"]
}
hardhat.config.ts:
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.20",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
hardhat: {
chainId: 1337,
},
},
};
export default config;
跑本地节点:
npx hardhat node
先从简单的ERC20燃烧开始,弄清楚基本逻辑。
contracts/BasicBurn.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract BasicBurnToken is ERC20, Ownable {
constructor() ERC20("BasicBurnToken", "BBT") Ownable() {
_mint(msg.sender, 1000000 * 10**decimals());
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
ERC20和Ownable。burn:调用ERC20._burn,减少调用者余额和totalSupply。_burn检查余额,防止下溢(Solidity 0.8.x)。burn,可能误操作。burn约30k Gas(1次SSTORE)。test/TokenBurn.test.ts:
import { ethers } from "hardhat";
import { expect } from "chai";
import { BasicBurnToken } from "../typechain-types";
describe("BasicBurn", function () {
let token: BasicBurnToken;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("BasicBurnToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.transfer(user1.address, ethers.utils.parseEther("1000"));
});
it("should burn tokens correctly", async function () {
const initialBalance = await token.balanceOf(user1.address);
const burnAmount = ethers.utils.parseEther("500");
await token.connect(user1).burn(burnAmount);
expect(await token.balanceOf(user1.address)).to.equal(initialBalance.sub(burnAmount));
expect(await token.totalSupply()).to.equal(ethers.utils.parseEther("999500"));
});
it("should revert if insufficient balance", async function () {
await expect(token.connect(user1).burn(ethers.utils.parseEther("2000"))).to.be.revertedWith("ERC20: burn amount exceeds balance");
});
});
跑测试:
npx hardhat test
totalSupply正确减少。_burn的安全检查。燃烧可能涉及外部调用(如通知),需防重入。
contracts/ReentrancyProtectedBurn.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract VulnerableBurn {
mapping(address => uint256) public balances;
uint256 public totalSupply;
constructor() {
balances[msg.sender] = 1000000 * 10**18;
totalSupply = 1000000 * 10**18;
}
function burn(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
(bool success, ) = msg.sender.call{value: 0}(abi.encodeWithSignature("notifyBurn(uint256)", amount));
require(success, "Notification failed");
balances[msg.sender] -= amount;
totalSupply -= amount;
}
}
burn先调用外部notifyBurn,后更新状态。notifyBurn再次调用burn,重复燃烧未更新的余额。burn,触发notifyBurn。notifyBurn再次调用burn,余额未更新,重复扣除。contracts/ReentrancyProtectedBurn.sol(update):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract SecureBurnToken is ERC20, ReentrancyGuard, Ownable {
event Burn(address indexed burner, uint256 amount);
constructor() ERC20("SecureBurnToken", "SBT") Ownable() {
_mint(msg.sender, 1000000 * 10**decimals());
}
function burn(uint256 amount) public nonReentrant {
_burn(msg.sender, amount);
emit Burn(msg.sender, amount);
(bool success, ) = msg.sender.call{value: 0}(abi.encodeWithSignature("notifyBurn(uint256)", amount));
require(success, "Notification failed");
}
}
ReentrancyGuard的nonReentrant修饰符,防止重复进入。_burn,更新状态,后发送通知。Burn事件,记录燃烧操作。nonReentrant锁住函数,防止重入。nonReentrant增加~2k Gas,安全性提升。test/TokenBurn.test.ts(add):
import { SecureBurnToken } from "../typechain-types";
describe("ReentrancyProtectedBurn", function () {
let token: SecureBurnToken;
let owner: any, attacker: any;
beforeEach(async function () {
[owner, attacker] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("SecureBurnToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.transfer(attacker.address, ethers.utils.parseEther("1000"));
const AttackerFactory = await ethers.getContractFactory("BurnAttacker");
const attackerContract = await AttackerFactory.deploy(token.address);
await attackerContract.deployed();
await token.transfer(attackerContract.address, ethers.utils.parseEther("1000"));
});
it("should prevent reentrancy attack", async function () {
const AttackerFactory = await ethers.getContractFactory("BurnAttacker");
const attackerContract = await AttackerFactory.deploy(token.address);
await expect(attackerContract.attack(ethers.utils.parseEther("500"))).to.be.revertedWith("ReentrancyGuard: reentrant call");
expect(await token.balanceOf(attackerContract.address)).to.equal(ethers.utils.parseEther("1000"));
});
it("should emit Burn event", async function () {
await expect(token.connect(attacker).burn(ethers.utils.parseEther("500")))
.to.emit(token, "Burn")
.withArgs(attacker.address, ethers.utils.parseEther("500"));
expect(await token.totalSupply()).to.equal(ethers.utils.parseEther("999500"));
});
});
contracts/BurnAttacker.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract BurnAttacker {
SecureBurnToken public token;
constructor(address _token) {
token = SecureBurnToken(_token);
}
function attack(uint256 amount) public {
token.burn(amount);
}
function notifyBurn(uint256 amount) external {
if (address(token).balance >= amount) {
token.burn(amount);
}
}
}
nonReentrant保护,失败。Burn事件,totalSupply正确减少。为燃烧加多签机制,需多人同意。
contracts/MultiSigBurn.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MultiSigBurnToken is ERC20, Ownable {
address[] public burners;
uint256 public required;
uint256 public transactionCount;
mapping(uint256 => Transaction) public transactions;
mapping(uint256 => mapping(address => bool)) public confirmations;
struct Transaction {
address burner;
uint256 amount;
bool executed;
uint256 confirmationCount;
}
event SubmitBurn(uint256 indexed txId, address indexed burner, uint256 amount);
event ConfirmBurn(uint256 indexed txId, address indexed burner);
event ExecuteBurn(uint256 indexed txId, address indexed burner, uint256 amount);
event RevokeConfirmation(uint256 indexed txId, address indexed burner);
event Burn(address indexed burner, uint256 amount);
modifier onlyBurner() {
bool isBurner = false;
for (uint256 i = 0; i < burners.length; i++) {
if (burners[i] == msg.sender) {
isBurner = true;
break;
}
}
require(isBurner, "Not burner");
_;
}
constructor(address[] memory _burners, uint256 _required) ERC20("MultiSigBurnToken", "MSBT") Ownable() {
require(_burners.length > 0, "Burners required");
require(_required > 0 && _required <= _burners.length, "Invalid required");
burners = _burners;
required = _required;
_mint(msg.sender, 1000000 * 10**decimals());
}
function submitBurn(address burner, uint256 amount) public onlyBurner {
require(balanceOf(burner) >= amount, "Insufficient balance");
uint256 txId = transactionCount++;
transactions[txId] = Transaction({
burner: burner,
amount: amount,
executed: false,
confirmationCount: 0
});
emit SubmitBurn(txId, burner, amount);
}
function confirmBurn(uint256 txId) public onlyBurner {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(!confirmations[txId][msg.sender], "Already confirmed");
confirmations[txId][msg.sender] = true;
transaction.confirmationCount++;
emit ConfirmBurn(txId, msg.sender);
if (transaction.confirmationCount >= required) {
executeBurn(txId);
}
}
function executeBurn(uint256 txId) internal {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(transaction.confirmationCount >= required, "Insufficient confirmations");
transaction.executed = true;
_burn(transaction.burner, transaction.amount);
emit ExecuteBurn(txId, transaction.burner, transaction.amount);
emit Burn(transaction.burner, transaction.amount);
}
function revokeConfirmation(uint256 txId) public onlyBurner {
Transaction storage transaction = transactions[txId];
require(!transaction.executed, "Transaction executed");
require(confirmations[txId][msg.sender], "Not confirmed");
confirmations[txId][msg.sender] = false;
transaction.confirmationCount--;
emit RevokeConfirmation(txId, msg.sender);
}
}
burners和required控制多签。submitBurn:提交燃烧提案。confirmBurn:确认提案,达标后执行。executeBurn:调用_burn销毁代币,触发Burn事件。revokeConfirmation:撤销确认。test/TokenBurn.test.ts(add):
import { MultiSigBurnToken } from "../typechain-types";
describe("MultiSigBurn", function () {
let token: MultiSigBurnToken;
let owner: any, burner1: any, burner2: any, burner3: any;
beforeEach(async function () {
[owner, burner1, burner2, burner3] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("MultiSigBurnToken");
token = await TokenFactory.deploy([burner1.address, burner2.address, burner3.address], 2);
await token.deployed();
await token.transfer(burner1.address, ethers.utils.parseEther("1000"));
});
it("should execute burn with multi-sig", async function () {
await token.connect(burner1).submitBurn(burner1.address, ethers.utils.parseEther("500"));
await token.connect(burner2).confirmBurn(0);
await expect(token.connect(burner3).confirmBurn(0))
.to.emit(token, "Burn")
.withArgs(burner1.address, ethers.utils.parseEther("500"));
expect(await token.balanceOf(burner1.address)).to.equal(ethers.utils.parseEther("500"));
expect(await token.totalSupply()).to.equal(ethers.utils.parseEther("999500"));
});
it("should not execute without enough confirmations", async function () {
await token.connect(burner1).submitBurn(burner1.address, ethers.utils.parseEther("500"));
await token.connect(burner2).confirmBurn(0);
expect(await token.balanceOf(burner1.address)).to.equal(ethers.utils.parseEther("1000"));
});
it("should allow revoking confirmation", async function () {
await token.connect(burner1).submitBurn(burner1.address, ethers.utils.parseEther("500"));
await token.connect(burner2).confirmBurn(0);
await token.connect(burner2).revokeConfirmation(0);
await token.connect(burner3).confirmBurn(0);
expect(await token.balanceOf(burner1.address)).to.equal(ethers.utils.parseEther("1000"));
});
});
totalSupply减少。支持一次烧多个账户的代币,适合大规模操作。
contracts/BatchBurn.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract BatchBurnToken is ERC20, Ownable {
event Burn(address indexed burner, uint256 amount);
constructor() ERC20("BatchBurnToken", "BBT") Ownable() {
_mint(msg.sender, 1000000 * 10**decimals());
}
function batchBurn(address[] memory accounts, uint256[] memory amounts) public onlyOwner {
require(accounts.length == amounts.length, "Invalid input");
for (uint256 i = 0; i < accounts.length; i++) {
_burn(accounts[i], amounts[i]);
emit Burn(accounts[i], amounts[i]);
}
}
}
batchBurn:批量销毁多个账户的代币。onlyOwner,防止滥用。Burn事件。onlyOwner控制权限。_burn验证余额。test/TokenBurn.test.ts(add):
import { BatchBurnToken } from "../typechain-types";
describe("BatchBurn", function () {
let token: BatchBurnToken;
let owner: any, user1: any, user2: any;
beforeEach(async function () {
[owner, user1, user2] = await ethers.getSigners();
const TokenFactory = await ethers.getContractFactory("BatchBurnToken");
token = await TokenFactory.deploy();
await token.deployed();
await token.transfer(user1.address, ethers.utils.parseEther("1000"));
await token.transfer(user2.address, ethers.utils.parseEther("1000"));
});
it("should batch burn tokens", async function () {
await expect(token.batchBurn(
[user1.address, user2.address],
[ethers.utils.parseEther("500"), ethers.utils.parseEther("300")]
))
.to.emit(token, "Burn")
.withArgs(user1.address, ethers.utils.parseEther("500"))
.to.emit(token, "Burn")
.withArgs(user2.address, ethers.utils.parseEther("300"));
expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("500"));
expect(await token.balanceOf(user2.address)).to.equal(ethers.utils.parseEther("700"));
expect(await token.totalSupply()).to.equal(ethers.utils.parseEther("999200"));
});
it("should revert if input arrays mismatch", async function () {
await expect(token.batchBurn(
[user1.address],
[ethers.utils.parseEther("500"), ethers.utils.parseEther("300")]
)).to.be.revertedWith("Invalid input");
});
});
totalSupply减少。燃烧代币换取奖励(如NFT或ETH),增加激励。
contracts/BurnWithReward.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract RewardNFT is ERC721, Ownable {
uint256 public tokenId;
constructor() ERC721("RewardNFT", "RNFT") Ownable() {}
function mint(address to) public onlyOwner returns (uint256) {
tokenId++;
_mint(to, tokenId);
return tokenId;
}
}
contract BurnWithRewardToken is ERC20, Ownable, ReentrancyGuard {
RewardNFT public nft;
uint256 public burnThreshold = 100 * 10**18; // 100 tokens for 1 NFT
uint256 public ethReward = 0.1 ether; // 0.1 ETH per 100 tokens
event Burn(address indexed burner, uint256 amount);
event Reward(address indexed user, uint256 tokenId);
event EthReward(address indexed user, uint256 amount);
constructor(address _nft) ERC20("BurnWithRewardToken", "BWRT") Ownable() {
nft = RewardNFT(_nft);
_mint(msg.sender, 1000000 * 10**decimals());
}
function burnForReward(uint256 amount) public nonReentrant payable {
require(amount >= burnThreshold, "Amount too low");
require(address(this).balance >= ethReward, "Insufficient ETH");
_burn(msg.sender, amount);
emit Burn(msg.sender, amount);
uint256 nftCount = amount / burnThreshold;
for (uint256 i = 0; i < nftCount; i++) {
uint256 newTokenId = nft.mint(msg.sender);
emit Reward(msg.sender, newTokenId);
}
uint256 ethAmount = (amount / burnThreshold) * ethReward;
(bool success, ) = msg.sender.call{value: ethAmount}("");
require(success, "ETH transfer failed");
emit EthReward(msg.sender, ethAmount);
}
function depositEth() public payable onlyOwner {}
}
burnForReward:燃烧代币换NFT和ETH。burnThreshold:100代币换1个NFT和0.1 ETH。nonReentrant防止重入。depositEth:为合约充值ETH。Burn、Reward、EthReward事件。nonReentrant保护外部调用。burnThreshold和ETH余额。test/TokenBurn.test.ts(add):
import { BurnWithRewardToken, RewardNFT } from "../typechain-types";
describe("BurnWithReward", function () {
let token: BurnWithRewardToken;
let nft: RewardNFT;
let owner: any, user1: any;
beforeEach(async function () {
[owner, user1] = await ethers.getSigners();
const NftFactory = await ethers.getContractFactory("RewardNFT");
nft = await NftFactory.deploy();
await nft.deployed();
const TokenFactory = await ethers.getContractFactory("BurnWithRewardToken");
token = await TokenFactory.deploy(nft.address);
await token.deployed();
await token.depositEth({ value: ethers.utils.parseEther("1") });
await token.transfer(user1.address, ethers.utils.parseEther("1000"));
});
it("should burn tokens and reward NFT and ETH", async function () {
const initialBalance = await ethers.provider.getBalance(user1.address);
await expect(token.connect(user1).burnForReward(ethers.utils.parseEther("200")))
.to.emit(token, "Burn")
.withArgs(user1.address, ethers.utils.parseEther("200"))
.to.emit(token, "Reward")
.withArgs(user1.address, 1)
.to.emit(token, "EthReward")
.withArgs(user1.address, ethers.utils.parseEther("0.2"));
expect(await token.balanceOf(user1.address)).to.equal(ethers.utils.parseEther("800"));
expect(await token.totalSupply()).to.equal(ethers.utils.parseEther("999800"));
expect(await nft.ownerOf(1)).to.equal(user1.address);
expect(await ethers.provider.getBalance(user1.address)).to.be.gt(initialBalance);
});
it("should revert if amount too low", async function () {
await expect(token.connect(user1).burnForReward(ethers.utils.parseEther("50"))).to.be.revertedWith("Amount too low");
});
});
burnThreshold失败。scripts/deploy.ts:
import { ethers } from "hardhat";
async function main() {
const [owner, burner1, burner2, burner3] = await ethers.getSigners();
const BasicBurnFactory = await ethers.getContractFactory("BasicBurnToken");
const basicBurn = await BasicBurnFactory.deploy();
await basicBurn.deployed();
console.log(`BasicBurnToken deployed to: ${basicBurn.address}`);
const SecureBurnFactory = await ethers.getContractFactory("SecureBurnToken");
const secureBurn = await SecureBurnFactory.deploy();
await secureBurn.deployed();
console.log(`SecureBurnToken deployed to: ${secureBurn.address}`);
const MultiSigBurnFactory = await ethers.getContractFactory("MultiSigBurnToken");
const multiSigBurn = await MultiSigBurnFactory.deploy([burner1.address, burner2.address, burner3.address], 2);
await multiSigBurn.deployed();
console.log(`MultiSigBurnToken deployed to: ${multiSigBurn.address}`);
const BatchBurnFactory = await ethers.getContractFactory("BatchBurnToken");
const batchBurn = await BatchBurnFactory.deploy();
await batchBurn.deployed();
console.log(`BatchBurnToken deployed to: ${batchBurn.address}`);
const RewardNftFactory = await ethers.getContractFactory("RewardNFT");
const rewardNft = await RewardNftFactory.deploy();
await rewardNft.deployed();
console.log(`RewardNFT deployed to: ${rewardNft.address}`);
const BurnWithRewardFactory = await ethers.getContractFactory("BurnWithRewardToken");
const burnWithReward = await BurnWithRewardFactory.deploy(rewardNft.address);
await burnWithReward.deployed();
console.log(`BurnWithRewardToken deployed to: ${burnWithReward.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
跑部署:
npx hardhat run scripts/deploy.ts --network hardhat
跑代码,体验Solidity代币燃烧!
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!