关于 token-antiflash
token-antiflash 是面向 ERC20 代币合约的闪电贷防护设计技能,供智能合约开发者评估代币层威胁并选择防护策略。它要求先讨论威胁模型、策略取舍和参数,再按确认结果实施。
主要功能
- 01评估代币威胁模型与攻击路径
- 02比较不同防护策略及其取舍
- 03涵盖成本追踪、冷却期和交易限额
- 04提供转账状态传播设计要点
- 05区分 OpenZeppelin 4.x 与 5.x 钩子
适用场景
- 01为 ERC20 代币设计闪电贷防护
- 02根据攻击路径选择防护组合
- 03确定限额、税率或冷却参数
- 04检查转账状态传播中的绕过风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ 0xlayerghost/ solidity-agent-kit --skill token-antiflash安装后在 Claude Code 中输入/token-antiflash确认已加载。
详细介绍
Token 级闪电贷防护设计模式
语言规则
- 始终使用与用户相同的语言回复。 如果用户使用中文提问,就用中文回复;如果使用英语提问,就用英语回复。
适用范围:适用于需要在代币合约层面防护闪电贷攻击的 ERC20 代币合约。本指南与
defi-security技能(协议层面)互为补充,介绍代币内部的防御机制。以下所有参数均为设计参考值——实际数值应根据项目需求、代币经济模型和市场状况确定。
工作流规则
触发此技能时,不要直接实现所有策略。请遵循以下工作流:
- 评估:识别项目的威胁模型——代币类型(Meme、社区、DeFi 生态系统)、面临风险的价值,以及现实可行的攻击途径
- 说明:向开发者展示策略决策矩阵和组合指南。清楚说明各策略的权衡(Gas 成本、用户体验摩擦、实现复杂度)
- 让开发者选择:询问开发者希望采用哪种防护级别(Basic / Standard / Advanced / Maximum),或希望采用哪些具体策略。不要擅自假设防护级别
- 确认参数:针对每项所选策略,在编写代码前与开发者确认关键设计参数(例如,税费档位、交易量限制百分比、冷却时间粒度)
- 实现:只有在开发者确认后,才使用双方商定的参数实现所选策略
例外:如果开发者明确要求“全部实现”或“最高防护”,则跳过步骤 2–4,直接实现 Maximum 组合。如果开发者按编号指定了确切策略,则针对这些策略跳到步骤 4。
版本兼容性
| OpenZeppelin 版本 | 要重写的钩子 | 备注 |
|---|---|---|
| OZ 5.x(Solidity ≥0.8.20) | _update(address from, address to, uint256 amount) | 钩子运行时,状态已更新 |
| OZ 4.x(Solidity <0.8.20) | _beforeTokenTransfer(address from, address to, uint256 amount) | 状态尚未更新;读取的是转账前的状态 |
// OZ 5.x — preferred for new projects
function _update(address from, address to, uint256 amount) internal override {
super._update(from, to, amount);
// ... defense logic here
}
// OZ 4.x — for existing projects
function _beforeTokenTransfer(address from, address to, uint256 amount) internal override {
super._beforeTokenTransfer(from, to, amount);
// ... defense logic here (balances not yet changed)
}
核心设计原则
闪电贷攻击依赖单笔交易中的原子执行:借入 → 操纵 → 获利 → 还款。代币层防护通过在代币合约内部引入成本、时间和身份屏障,打破这种原子性。
策略决策矩阵
设计带闪电贷防护的代币时,应根据威胁模型评估要采用哪些策略:
| 威胁途径 | 推荐策略 | 优先级 |
|---|---|---|
| 同一区块内买入→卖出套利 | 同区块冷却 + 合约地址检查 | 高 |
| 零成本代币利用(空投/漏洞利用) | 成本追踪 + 渐进式卖出税 | 高 |
| 大额交易操纵 | 按地址限制交易量 + 基础交易费 | 中 |
| 多地址 Sybil 绕过 | 推荐关系绑定 + 转账时状态传播 | 中 |
| Bot 抢先交易 / 三明治攻击 | 抢先交易卖出保护 + 动态费用 | 视情况而定 |
策略 1:成本追踪
设计思路:追踪每个地址持有代币的取得成本。将已实现利润限制为成本的可配置倍数。零成本取得的代币(空投、漏洞利用、闪电贷)成本为零,这意味着利润上限为零,或出售时销毁代币。
关键设计要点:
- 维护每个地址的成本状态,并在每次
_transfer/_update时更新 - 转账时:成本随代币转移——按比例将成本从发送方传递给接收方
- 应标记零成本代币,并单独处理(销毁或限制出售)
- 利润上限倍数应为可配置的所有者参数,并设置合理的上限
- 考虑维护额外映射所产生的 Gas 成本——测量对转账 Gas 的影响
为何能防御闪电贷:闪电贷借入的代币天然具有零成本。由于利润受成本限制,执行攻击便没有经济动机。
实现:
struct CostBasis {
uint256 totalCost; // total acquisition cost in quote token units
uint256 totalAmount; // total token amount held
}
mapping(address => CostBasis) private _costBasis;
function _propagateCostBasis(address from, address to, uint256 amount) private {
CostBasis storage senderCost = _costBasis[from];
if (senderCost.totalAmount == 0) return;
// proportional cost transfer
uint256 transferredCost = (senderCost.totalCost * amount) / senderCost.totalAmount;
senderCost.totalCost -= transferredCost;
senderCost.totalAmount -= amount;
_costBasis[to].totalCost += transferredCost;
_costBasis[to].totalAmount += amount;
}
// call this when a buy is detected (from == AMM pair)
function _recordBuy(address buyer, uint256 amount, uint256 costInQuote) private {
_costBasis[buyer].totalCost += costInQuote;
_costBasis[buyer].totalAmount += amount;
}
策略 2:合约地址检查(兼容 EIP-7702)
设计思路:阻止智能合约(闪电贷合约即属此类)发起交互,同时正确处理 EIP-7702 委托的 EOA。
关键设计要点:
- EIP-7702 委托标识符具有特定的代码长度(23 字节:
0xef0100+ 20 字节地址) - 允许代码长度恰好为此值的地址(它们是委托 EOA,而非合约)
- 阻止代码长度不为零且不符合上述值的地址(普通智能合约)
- 还可考虑将
msg.sender == tx.origin作为辅助防护 - 将必要的合约加入白名单:LP 交易对、路由器、质押合约
为何能防御闪电贷:闪电贷在合约调用链中执行。阻止合约调用方会迫使交互通过 EOA 进行,从而打破原子化执行。
实现:
// EIP-7702 delegation designator format:
// byte[0..2] = 0xef0100 (magic prefix, 3 bytes)
// byte[3..22] = target contract address (20 bytes)
// total: 23 bytes
// Plain EOA has extcodesize = 0.
// Regular contract has extcodesize > 0 (and != 23 for the magic prefix case).
bytes3 private constant EIP7702_MAGIC = 0xef0100;
/// @dev Classifies an address into three types:
/// - size == 0 → plain EOA → return false (allow)
/// - size == 23 AND prefix matches 0xef0100 → EIP-7702 delegated EOA → return false (allow)
/// - anything else → real deployed contract → return true (block)
///
/// EIP-7702 delegated EOAs are allowed because:
/// 1. They are user-controlled accounts (smart wallets), not attack contracts
/// 2. They cannot execute atomic flash loan buy→sell (Strategy 3 still blocks same-block ops)
/// 3. Blocking them would break smart wallet UX for legitimate holders
///
/// extcodecopy gas note: ~700 gas cold read. Only runs when size == 23, which is rare.
function _isRealContract(address account) internal view returns (bool) {
uint256 size;
assembly { size := extcodesize(account) }
// fast path: most addresses are plain EOA or regular contract
if (size == 0) return false; // plain EOA → allow
if (size != 23) return true; // regular contract (size > 23 or 1..22) → block
// slow path: exactly 23 bytes → read prefix to distinguish 7702 from an exotic 23-byte contract
bytes3 prefix;
assembly {
// extcodecopy(addr, destMemPtr, srcOffset, length)
// write 3 bytes starting at offset 0 of the account's code into `prefix`
extcodecopy(account, prefix, 0, 3)
}
if (prefix == EIP7702_MAGIC) return false; // confirmed EIP-7702 delegated EOA → allow
return true; // exotic 23-byte contract → block
}
/// @dev Call at the top of _update (or _beforeTokenTransfer) to block contract interactions.
/// Whitelisted addresses (LP pair, router, staking) bypass this check entirely.
function _checkNotContract(address addr) internal view {
if (_isWhitelisted[addr]) return;
require(!_isRealContract(addr), "AntiFlash: contract caller blocked");
}
策略 3:同区块冷却
设计思路:禁止在同一区块内买入后卖出(或收到代币后卖出)。这是防御闪电贷最直接的方法,因为闪电贷必须在单笔交易(一个区块)内完成。
关键设计要点:
- 追踪每个地址最近一次收到代币时的区块号
- 卖出时(转账至 LP 交易对):要求追踪的区块号严格小于当前的
block.number - 关键:还要处理转账绕过的情况——阻止在同一区块内执行
A 买入 → A 转账给 B → B 卖出 - 钱包间转账时:使用
max(lastBlock[to], lastBlock[from])传递区块限制,以防止状态洗白 - 不要使用
block.timestamp追踪同一区块——block.number才是正确的指标
为何能防御闪电贷:闪电贷在单笔交易(单一区块)内执行并偿还。同区块冷却可彻底阻止买入→卖出循环。
_update 集成枢纽
所有策略状态的传递都必须整合到单个 _update 重写中。这是最常被忽略的漏洞——只接入部分逻辑会留下可绕过转账的路径。
function _update(address from, address to, uint256 amount) internal override {
// ── Guard: call super first so balances are updated before our checks ──
super._update(from, to, amount);
bool fromPair = _isAMMPair[from]; // buy: tokens flow from pair → user
bool toPair = _isAMMPair[to]; // sell: tokens flow from user → pair
bool isTransfer = !fromPair && !toPair && from != address(0) && to != address(0);
// ── Strategy 2: Block real contracts ──
// Only check non-whitelisted, non-pair addresses
if (from != address(0) && !fromPair) {
_checkNotContract(from);
}
// ── Strategy 3: Same-block cooldown ──
if (toPair && from != address(0)) {
// sell path: seller must not have received tokens this block
require(
block.number > _lastInteractionBlock[from],
"AntiFlash: same-block sell"
);
}
if (fromPair) {
// buy path: record the block so this address cannot sell same-block
_lastInteractionBlock[to] = block.number;
}
if (isTransfer) {
// ★ transfer path: propagate the stricter (larger) block number
// using max() ensures restriction only extends, never shortens
// prevents: buy → transfer to fresh address → sell same-block
if (_lastInteractionBlock[from] > _lastInteractionBlock[to]) {
_lastInteractionBlock[to] = _lastInteractionBlock[from];
}
}
// ── Strategy 1: Cost basis propagation ──
if (useCostTracking && isTransfer) {
_propagateCostBasis(from, to, amount);
}
if (useCostTracking && fromPair) {
// record buy cost using current price from oracle or passed-in value
// (exact cost recording depends on your price feed integration)
_recordBuy(to, amount, _getCurrentCostBasis(amount));
}
// ── Strategy 7: Progressive tax timestamp propagation ──
if (useProgressiveTax && isTransfer) {
// propagate the older (more restrictive) timestamp so receiver
// cannot launder holding time by receiving a fresh transfer
if (_firstBuyTimestamp[from] > 0) {
if (_firstBuyTimestamp[to] == 0 || _firstBuyTimestamp[from] > _firstBuyTimestamp[to]) {
_firstBuyTimestamp[to] = _firstBuyTimestamp[from];
}
}
}
if (useProgressiveTax && fromPair && _firstBuyTimestamp[to] == 0) {
_firstBuyTimestamp[to] = block.timestamp;
}
// ── Strategy 4: Per-address daily volume ──
if (useDailyLimit && !fromPair && from != address(0)) {
uint256 today = block.timestamp / 1 days;
_dailyVolume[from][today] += amount;
require(_dailyVolume[from][today] <= _dailyLimit, "AntiFlash: daily limit");
}
}
所有状态传递的关键规则:使用
max()语义——限制只延长,绝不缩短。直接赋值(state[to] = state[from])可被利用:冷却时间较短的发送方可以覆盖接收方较长的冷却时间。
策略 4:单地址交易量限制
设计思路:从单笔交易和周期两个层面限制个人交易量,降低任何攻击的经济规模。
关键设计要点:
- 单笔交易限额:可配置为总供应量的一定比例
- 周期(例如每日)内的单地址累计限额
- 使用
block.timestamp / period_length作为映射键来跟踪周期 - 将 LP 交易对、质押合约和运营地址列入白名单,使其不受限制
- 参数应允许所有者调整,并设定上限
抵御闪电贷攻击的原理:限制攻击者在单笔交易中可转移的数量。即使其他防御措施被绕过,损失也能受到限制。
策略 5:最低余额保留
设计思路:要求每次转账后,发送方账户中都保留少量最低余额,而不是允许全部提取。
关键设计要点:
- 定义可配置的最低保留余额(少量尘埃金额)
- 在
_update/_transfer中强制执行:balanceOf(from) - amount >= minBalance - 特定操作可例外:授权销毁、合约迁移
- 附带好处:维持持币者数量指标(地址余额不会归零)
- 最低余额应足够小,不影响正常用户体验
策略 6:抢先交易卖出保护
设计思路:检测到大额卖单时,调整参数或触发保护机制,以缓冲价格冲击。
关键设计要点:
- 定义触发保护的阈值(例如 LP 储备的一定比例)
- 可选方案:临时提高卖出手续费、用已收税款回购、调整 LP 参数
- 最好在业务/路由器层实现,而不是在代币合约内部实现
- 考虑使用 Uniswap V4 hooks 实现原生 LP 集成
- 按时间窗口监控卖出与买入的比例;当卖出比例明显偏高时,提高手续费
- 此策略较为复杂,可能不适用于所有项目
策略 7:递进式卖出税(阶梯税)
设计思路:对近期获得的代币征收较高卖出税,并随着持有时间增加而降低税率。鼓励持有,惩罚快速买卖。
关键设计要点:
- 跟踪每个地址的首次买入时间戳
- 定义可配置的税率计划:持有时间最短时税率最高,随后逐步降至基准税率
- 从卖出金额中扣除税款,并发送至指定的手续费接收方
- 关键:钱包到钱包转账时,传递时间戳以防止时间戳洗白(如果发送方受到限制,接收方不应获得“全新”的时间戳)
- 税务参数应允许所有者调整
- 考虑使用时间分档(小时/天),而非连续公式,以提高燃料效率
抵御闪电贷攻击的原理:闪电借入的代币实际持有时间接近于零 → 适用最高税率 → 消除利润空间。
实现方式:
struct TaxTier {
uint32 maxAge; // max holding time in seconds for this tier to apply
uint16 taxBps; // tax rate in basis points (e.g. 2000 = 20%)
}
TaxTier[] public sellTaxSchedule;
/// @dev Returns the sell tax rate (bps) for `seller` based on their holding time.
/// Tiers must be ordered ascending by maxAge. Returns BASE_TAX_BPS if all tiers exceeded.
function _getSellTaxRate(address seller) internal view returns (uint256) {
uint256 holdTime = block.timestamp - _firstBuyTimestamp[seller];
uint256 len = sellTaxSchedule.length;
for (uint256 i; i < len; ) {
if (holdTime < sellTaxSchedule[i].maxAge) return sellTaxSchedule[i].taxBps;
unchecked { ++i; }
}
return BASE_TAX_BPS; // long-term holder baseline rate
}
/// @dev Example schedule — owner should configure via setter with an upper-bound guard
function _initDefaultTaxSchedule() internal {
sellTaxSchedule.push(TaxTier(1 hours, 4000)); // < 1h → 40%
sellTaxSchedule.push(TaxTier(1 days, 2500)); // < 1d → 25%
sellTaxSchedule.push(TaxTier(7 days, 1500)); // < 7d → 15%
sellTaxSchedule.push(TaxTier(30 days, 500)); // < 30d → 5%
// > 30d → BASE_TAX_BPS
}
/// @dev Owner setter — enforce upper bound to prevent rug via tax
function setSellTaxSchedule(TaxTier[] calldata tiers) external onlyOwner {
for (uint256 i; i < tiers.length; ) {
require(tiers[i].taxBps <= 5000, "AntiFlash: tax exceeds 50%");
unchecked { ++i; }
}
delete sellTaxSchedule;
for (uint256 i; i < tiers.length; ) {
sellTaxSchedule.push(tiers[i]);
unchecked { ++i; }
}
}
燃料说明:对于 ≤8 个档位,线性扫描即可。如果需要更多档位,请改用二分查找或硬编码的
if-else链。
策略 8:生态系统收益税
设计思路:额外的卖出税用于累积生态系统价值,例如 NFT 持有者分红、子代币销毁或其他生态系统合约。
关键设计要点:
- 在基础费用之外额外征收卖出税,并分配给指定的生态系统合约
- 税款可根据已实现利润计算(需要使用策略 1 的成本跟踪),也可采用固定税率
- 通过**提取模式(领取)**而非推送模式分发分红,以避免燃料消耗攻击
- 可与策略 7 结合,构建多层税收体系
- 确保税款接收合约经过验证且不可变(或受时间锁保护)
策略 9:基础交易手续费
设计思路:对所有买入/卖出交易收取标准基础手续费,并结合 AMM 自然滑点,为任何往返交易设定最低成本门槛。
关键设计要点:
- 对买入和卖出方向均收取可配置的手续费
- 手续费分配给:开发基金/注入 LP/销毁(可配置)
- 与 AMM 滑点结合,为往返交易设定最低成本
- 单靠这一策略不足以防范闪电贷——它是基础防护层,需与其他策略配合使用
- 手续费应保持合理,避免阻碍正常交易
策略 10:推荐关系绑定系统
设计思路:要求地址在交易前建立推荐关系,形成匿名闪电贷合约无法绕过的身份门槛。
关键设计要点:
- 新地址必须先由现有推荐人“激活”,才能进行首次交易
- 每个地址的推荐关系仅可设置一次,且不可更改
- 从交易手续费中支付多级推荐佣金(深度和费率可配置)
- 闪电贷合约没有推荐人 → 无法交易
- 提供一定程度的 Sybil 抵抗能力(短时间内创建大量已激活地址更加困难)
- 考虑对用户体验的影响——这可能会给合法新用户带来阻碍
组合指南
根据项目需求选择策略。策略越多,防护越强,但 Gas 成本和实现复杂度也越高:
| 防护级别 | 策略 | 适用场景 |
|---|---|---|
| 基础 | 3 + 4 + 9 | 有基本防护需求的标准代币 |
| 标准 | 2 + 3 + 4 + 5 + 7 + 9 | 有一定价值风险敞口的社区代币 |
| 高级 | 1 + 2 + 3 + 4 + 5 + 7 + 8 + 9 | 拥有生态系统的高价值 DeFi 代币 |
| 最高 | 所有策略 | 拥有 NFT 生态系统、子代币且 TVL 较高的代币 |
Gas 优化:结构体打包
启用多种策略时,每个独立的 mapping(address => uint256) 在每次转账时都会对每个字段产生一次 SLOAD/SSTORE。将每个地址的所有状态打包到一个结构体中,可将其减少为一次存储槽读写。
// ❌ Scattered storage — up to 4 separate SLOAD/SSTORE per transfer
mapping(address => uint256) private _lastInteractionBlock; // slot A
mapping(address => uint256) private _firstBuyTimestamp; // slot B
mapping(address => uint256) private _dailyVolume; // slot C
mapping(address => uint256) private _cooldownUntil; // slot D
// ✅ Packed storage — 1 SLOAD + 1 SSTORE covers all four fields
// uint64 block number: safe until block ~1.8×10^19 (far future)
// uint64 timestamp: safe until year ~584 billion
// uint96 daily volume: up to ~79 billion tokens with 18 decimals
struct UserState {
uint64 lastBlock; // Strategy 3: last interaction block
uint64 firstBuyTime; // Strategy 7: first buy timestamp
uint96 dailyVolume; // Strategy 4: today's traded volume
uint32 cooldownUntil; // optional: unix timestamp cooldown end
// total: 8+8+12+4 = 32 bytes → exactly 1 storage slot
}
mapping(address => UserState) private _userState;
// Example read — one SLOAD fetches all fields
function _getSellTaxRatePacked(address seller) internal view returns (uint256) {
uint256 holdTime = block.timestamp - uint256(_userState[seller].firstBuyTime);
// ... same tier lookup as Strategy 7
}
// Example write — one SSTORE updates all touched fields
function _updateOnBuy(address buyer) internal {
UserState storage s = _userState[buyer];
s.lastBlock = uint64(block.number);
s.firstBuyTime = s.firstBuyTime == 0 ? uint64(block.timestamp) : s.firstBuyTime;
// dailyVolume and cooldownUntil updated separately as needed
}
适用场景:当启用 3 种以上策略且需要关注转账 Gas 消耗时,启用结构体打包。请测量优化前后的差异——根据存储槽的冷/热状态,每次转账通常可节省 10,000–40,000 Gas。
实现检查清单
部署具有反闪电贷保护的代币前:
- 所有防护状态(成本基准、区块跟踪、时间戳)均在所有转账路径(包括
_transfer/_update)中正确传递 - 钱包间转账会传递限制状态——不仅限于与交易对的交互(这是最常被忽略的漏洞)
- 为以下对象正确配置白名单:LP 交易对、路由器、质押合约、部署者多签钱包
- 所有可配置参数均设有所有者设置函数,并定义上下限
- 如果使用合约地址检查,需正确处理 EIP-7702 委托标识(23 字节)
- 同区块冷却机制同时覆盖直接卖出和先转账再卖出的模式
- 渐进式税费的时间戳无法通过转账到新地址来重置
- 所有手续费/税款收取均使用拉取模式(领取),而非推送模式
- 测量额外跟踪映射带来的 Gas 影响——确保
_transfer不超过可接受的 Gas 限制 - 模糊测试覆盖:零成本代币卖出、同区块多跳、地址循环、多地址 Sybil 绕过尝试
- 如果涉及依赖预言机的定价,应在业务层考虑集成 TWAP(难以直接在代币合约中实现——优先使用路由器/业务合约)