LAK 合约审查报告
开发者导向的合约行为说明 · 不等同于安全审计
| 项目 | 内容 |
|---|---|
| 审查对象 | LAK @ 0xb25E69F0D1f07A4e473DAdf93Af5f9322AA5DBF6(Sepolia,chainId 11155111) |
| 源码来源 | 链上已验证源码,经字节码比对确认与仓库源码一致(见第 2 节)—— ⚠️ 该一致性现仅对 2026-09-17 修复前的提交成立 |
| 审查方式 | 源码逐行精读 + 链上元数据核验;不含动态测试、模糊测试或形式化验证 |
| 报告性质 | 不是安全审计、不是安全认证、不是形式化验证。仅说明"合约做了什么、谁能做什么、资产如何流动" |
| 证据规则 | 所有断言标注代码位置;名称与注释仅作线索,行为以实现为准 |
| 报告日期 | 2026-09-16(2026-09-17 更新:6.3 修复落地,见 6.3 节) |
| 配套文件 | AUDIT-SELF-CHECK.md(工具驱动的自查)、ISSUANCE-STRUCTURE.md(治理事实)、WHITEPAPER.md(对外口径) |
⚠️ 源码修订影响(2026-09-17):6.3 的修复已合入仓库源码,但尚未部署。 Sepolia 上的
0xb25E69…DBF6仍是修复前的字节码,与当前仓库源码不再逐位一致。 第 2 节的一致性核验结论只对修复前的提交成立;下一次部署(含主网)必须重新核验。
1. 结论摘要
| 维度 | 结论 |
|---|---|
| 合约类型 | ERC-20 实用型代币 + 角色权限治理 + 行情联动销毁;非代理、不可升级 |
| 继承来源 | 全部为 OpenZeppelin v5.0.2 标准实现;项目自定义逻辑 5 处(见第 4 节) |
| 资产流向 | 无原生资产托管;唯一外部调用是 rescueToken(受 DEFAULT_ADMIN_ROLE + nonReentrant 约束) |
| 特权能力 | 7 类角色,全部可移交多签(当前已全部移交 3 持有人 / 2 签多签) |
| 链上约束 | 硬顶 / 费率上限 / 销毁率上限 / 指数与灵敏度上限均为常量,超限交易直接回滚 |
| 本次新发现 | 1 项值得注意的风险点(见 6.3:治理参数可导致转账全局回滚),此前自查报告未覆盖 —— 已于 2026-09-17 在仓库源码修复,待下次部署生效 |
2. 链上一致性核验(本次新增证据)
方法:拉取链上运行时字节码,与仓库编译产物 artifacts/LAK.sol/LAK.json 的 deployedBytecode 逐位比对。
| 项 | 结果 |
|---|---|
| 字节码长度 | 链上 10,520 字节 = 本地 10,520 字节 |
| 逐位比对 | 差异仅出现在 immutable 填充位置 |
| 差异来源 | OZ utils/cryptography/EIP712.sol 的 7 个 immutable(_cachedChainId = 11155111、_cachedThis = 合约地址、_cachedDomainSeparator、_hashedName、_hashedVersion、_name、_version)—— 编译期为占位符、部署期写入,属预期行为 |
| 归一化后比对 | ✅ 完全一致 |
链上元数据(Etherscan)
| 项 | 值 |
|---|---|
| ContractName | LAK |
| Compiler | v0.8.24+commit.e11b9ed9 |
| Optimization | 开启,runs = 200 |
| EVM Version | paris |
| Proxy | 否 |
| Library | 无 |
| 源码大小 | 128,721 字节 |
✅ 结论:链上运行的代码与仓库源码编译结果一致。 这一核验为后续"以仓库源码为审查依据"提供了依据。
3. 架构与继承
LAK
├── ERC20 (OZ) 基础代币
├── ERC20Burnable (OZ) 持有人自毁
├── ERC20Pausable (OZ) 暂停
├── ERC20Permit (OZ) EIP-2612 离线授权
├── AccessControl (OZ) 七类角色
└── ReentrancyGuard (OZ) 重入防护
重写点:decimals() / _update()
新增点:角色常量、经济参数、风控状态、事件、错误、业务函数
唯一状态变更入口:_update(from, to, value) —— 所有转账、铸造、销毁都经此函数
(contracts/LAK.sol:278),这是理解全部分发逻辑的关键。
4. 自定义逻辑清单(相对 OZ 标准实现的部分)
| # | 位置 | 内容 |
|---|---|---|
| 1 | constructor (:111) |
一次性授予 7 类角色给 admin;设置 feeRecipient;把 admin 与合约自身加入手续费豁免;可选创世铸造 |
| 2 | _update (:278) |
黑名单校验 → 行情销毁 → 手续费 → 净额到账(三段式 _update 调用) |
| 3 | currentBurnBps (:229) |
销毁率计算:基础值 + 加权指数偏离 × 敏感度,封顶 maxBurnBps |
| 4 | batchTransfer (:245) |
循环 _transfer,nonReentrant 保护 |
| 5 | rescueToken (:261) |
归集误入的其他 ERC-20;显式禁止操作 LAK 自身 |
5. 参与者与可执行动作
5.1 任何持币人(无需角色)
| 动作 | 入口 | 说明 |
|---|---|---|
| 转账 | transfer / transferFrom |
经 _update,可能触发销毁与手续费 |
| 自毁代币 | burn |
OZ ERC20Burnable;不计入 totalBurned(见 6.2) |
| 授权 | approve / permit |
permit 为一次签名完成离线授权 |
| 批量分发 | batchTransfer (:245) |
数组长度必须一致,否则 ArrayLengthMismatch |
5.2 MINTER_ROLE
| 动作 | 约束 |
|---|---|
mint(to, amount) (:139) |
to ≠ 0;to 未被冻结;totalSupply() + amount ≤ MAX_SUPPLY(常量 21,000,000) |
5.3 BURNER_ROLE
| 动作 | 约束 |
|---|---|
burnFromAccount(account, amount) (:147) |
account ≠ 0;余额不足由 _burn 回滚;累加 totalBurned |
setBurnPolicy(...) (:209) |
maxBurnBps ≤ 500;baseBurnBps ≤ maxBurnBps;musicWeightBps ≤ 10000 |
5.4 PAUSER_ROLE
| 动作 | 说明 |
|---|---|
pause() / unpause() (:156、:160) |
两个函数共用同一角色,无法拆分"暂停即时、恢复需审批" |
5.5 COMPLIANCE_ROLE
| 动作 | 说明 |
|---|---|
setBlocked(account, bool) (:165) |
被冻结地址无法转出、转入、被增发(_update 与 mint 双重校验) |
5.6 FEE_ROLE
| 动作 | 约束 |
|---|---|
setFeeBps (:173) |
≤ 500(5%) |
setFeeRecipient (:179) |
≠ 0 |
setFeeExempt(account, bool) (:185) |
豁免同时免除销毁与手续费(见 6.4) |
5.7 ORACLE_ROLE
| 动作 | 约束 |
|---|---|
setIndustryIndex(ent, music) (:198) |
两者均 ≠ 0;无上限(见 6.3) |
5.8 DEFAULT_ADMIN_ROLE
| 动作 | 说明 |
|---|---|
grantRole / revokeRole |
可授予或撤销任意角色(含自身)—— OZ 标准行为 |
renounceRole |
仅能放弃自己的角色(OZ v5 要求 account == msg.sender) |
rescueToken(token, to, amount) (:261) |
归集误入资产;禁止 token == address(this);to ≠ 0;nonReentrant |
6. 关键实现细节与观察
6.1 _update 的三段式扣减(✅ 确认行为)
// :292-293 计提
if (burnRate > 0) burnAmount = (value * burnRate) / BPS;
if (feeBps > 0) feeAmount = ((value - burnAmount) * feeBps) / BPS;
// :296-299 销毁(总供应下降)
super._update(from, address(0), burnAmount);
// :302-306 手续费(进入 feeRecipient)
super._update(from, recipient, feeAmount);
// :308 净额到账
super._update(from, to, value - burnAmount - feeAmount);
| 观察 | 说明 |
|---|---|
| 计费顺序 | 手续费按"销毁后金额"计提 —— 与白皮书 4.4 节「先销毁、再计费、最后净额到账」完全一致 |
| 扣减守恒 | burnAmount + feeAmount + (value − burnAmount − feeAmount) = value ✓ |
| 计费条件 | 仅当 from 与 to 双方都不豁免 且 value > 0 时计费(:290) |
| gas 代价 | 三段式意味着最多 3 次 _update;开启销毁与手续费时,单笔转账的 gas 约为普通 ERC-20 的 2–3 倍 |
| 铸造/销毁 | from == 0 或 to == 0 时 isTransfer = false → 不产生销毁与手续费 ✓ |
6.2 🟡 totalBurned 的统计口径不完整
| 销毁路径 | 是否计入 totalBurned |
|---|---|
_update 中的行情销毁(:298) |
✅ 计入 |
burnFromAccount(:150) |
✅ 计入 |
burn()(OZ ERC20Burnable,持有人自毁) |
❌ 不计入 |
影响:totalBurned 是"协议强制销毁量",不等于代币的累计销毁总量。
若对外以 totalBurned 作为"通缩证据",口径会偏低。
建议:对外表述改为「协议销毁量」,或在前端同时展示 MAX_SUPPLY − totalSupply() 作为总销毁量。
(属语义/展示问题,非安全缺陷。)
6.3 ✅ 治理参数可导致转账全局回滚(本次新发现 · 已在源码修复)
修复已落地(2026-09-17,仓库源码;尚未部署)
- 指数上限:新增常量
MAX_INDUSTRY_INDEX = 10 × INDEX_SCALE(基准 100 → 上限 1000),setIndustryIndex两个参数均校验- 灵敏度上限:新增常量
MAX_BURN_SENSITIVITY = BPS,setBurnPolicy超限 revertSensitivityTooHigh- 计算侧防御:
currentBurnBps()对devEntertainment/devMusic/weightedDev按MAX_DEV_BPS封顶 —— 即使将来新增写入路径绕过入口校验,也不可能溢出- 上限以链上常量强制,与合约既有风格一致(硬顶 / 费率上限 / 销毁率上限同为常量)
- 回归测试
test/LAK.burn-params.test.js已从「记录缺陷行为」改为「断言拒绝」,并新增 「把每个参数都推到允许的最大值后currentBurnBps不溢出、转账始终可用」一例- 全量 54 项测试通过(本文件 12 项);合约体积 14,428 / 24,576 字节(58.7%)
以下为修复前的原始记录,保留以便对照。
currentBurnBps()(:229)在三处做了算术,其中两处没有上限约束:
uint256 devEntertainment = entertainmentIndex > indexTarget ? ((entertainmentIndex - indexTarget) * BPS) / indexTarget : 0;
uint256 weightedDev = (devEntertainment * (BPS - musicWeightBps) + devMusic * musicWeightBps) / BPS;
uint256 total = baseBurnBps + (weightedDev * burnSensitivity) / BPS; // ← burnSensitivity 无上限
约束缺失点
| 变量 | 约束情况 |
|---|---|
entertainmentIndex / musicIndex |
仅校验 ≠ 0,无上限(:202) |
burnSensitivity |
完全无约束(setBurnPolicy 未校验,:216-218 只校验了另外三个参数) |
maxBurnBps |
✅ 有上限(≤ 500) |
后果(已实证)
以下结论由
test/LAK.burn-params.test.js在本地 Hardhat 网络复现,非纯推演。
若 autoBurnEnabled = true,且治理方设定了大额 entertainmentIndex 与 burnSensitivity:
weightedDev * burnSensitivity → 超过 2^256 → Solidity 0.8 溢出检查 → revert
由于 currentBurnBps() 被 _update(:291)在符合计费条件的每一笔转账中调用:
| 实测结论 | 结果 |
|---|---|
currentBurnBps() 本身 |
✅ 确认回滚 |
| 符合计费条件的普通转账(双方均未豁免) | ✅ 确认一并回滚 |
pause() + unpause() 之后 |
✅ 确认仍然回滚(问题在算术,不在暂停状态) |
| 治理方把参数调回合理区间后 | ✅ 确认恢复正常 |
等价于一次
unpause无法恢复的链上冻结,必须靠额外治理操作才能恢复可用性。
风险评级:低(需要 ORACLE_ROLE + BURNER_ROLE 同时设置极端参数,而两者当前均由多签持有),
但后果严重,且此前的自查报告(Solhint / Slither / 覆盖率)未覆盖这一类"参数组合导致运行期回滚"的问题 ——
自动化工具检查的是代码缺陷,而这是参数组合造成的可用性故障。
建议
- 主网版本为
entertainmentIndex/musicIndex增加合理上限(如 ≤ 100 × INDEX_SCALE 量级); - 为
burnSensitivity增加上限校验; - 在
setIndustryIndex/setBurnPolicy中加入一次"试算"(调用内部计算并确认不溢出); - 运营侧设定硬性规则:修改参数的同一笔治理操作应先在小额转账上验证。
6.4 🟡 手续费豁免同时免除销毁
:290 的条件是「双方都不豁免才计费」,意味着任一豁免地址参与的交易:
- 不产生手续费 ✓(设计意图:避免做市磨损,见白皮书 4.4)
- 也不产生行情销毁 ← 容易被忽略的一点
影响:若把大量地址(如运营账户、合作方)加入豁免名单,通缩机制的实际作用范围会被显著缩小。
建议:豁免名单应保持最小化并对外公示;白皮书中"零手续费场景"的表述可补充"同时不计入销毁"。
6.5 🟡 rescueToken 的信任边界
IERC20(token).safeTransfer(to, amount); // :268
token 为调用方指定的任意地址,理论上可以是恶意合约。现有防护:
| 防护 | 来源 |
|---|---|
| 返回值校验 | OZ SafeERC20 |
| 重入保护 | nonReentrant |
| 禁止操作本币 | token == address(this) → RescueNotAllowed |
判定:风险可接受。 该函数仅能由 DEFAULT_ADMIN_ROLE(多签)调用,且调用方需主动指定恶意地址。
6.6 🟡 冗余防御分支
:301 feeRecipient == address(0) ? address(0) : feeRecipient —— 由于构造函数与 setFeeRecipient 均拒绝零地址,该分支逻辑不可达(与自查报告 5.3 一致)。保留无害,可精简。
6.7 ✅ 与各文档的一致性核对
| 检查项 | 结果 |
|---|---|
| 硬顶 21,000,000 | ✅ 常量 MAX_SUPPLY(:43),mint 强制校验 |
| 费率上限 5% | ✅ 常量 MAX_FEE_BPS(:45),setFeeBps 强制 |
| 销毁率上限 5% | ✅ 常量 MAX_BURN_BPS(:46),setBurnPolicy 强制 |
| 七类角色 | ✅ 与白皮书 8.1 节、README 角色矩阵一致 |
| 结算顺序(先销毁后计费) | ✅ 与白皮书 4.4 节一致 |
indexTarget 为常量 |
✅(:66),与自查报告 5.1「已实施」一致 |
| 合约注释称「项目主体位于老挝」 | 🟡 与 ISSUANCE-STRUCTURE.md「主体尚待登记」措辞存在张力。属注释表述,因合约已部署且不可升级,不建议为此重新部署;对外材料以 ISSUANCE-STRUCTURE.md 为准 |
7. 未覆盖范围与不确定性
本报告未做的事(不代表已完成):
- ❌ 未做动态测试、模糊测试(fuzzing)、形式化验证
- ❌ 未审计 OpenZeppelin 依赖本身(假定其正确性由上游审计背书)
- ❌ 未评估经济模型、代币分配与市场行为的合理性
- ❌ 未做跨链组件(Solana / BRC-20)审查
- ❌ 未评估治理流程、密钥管理与多签操作规范
- ❌ 未评估合规与监管定性(见
ISSUANCE-STRUCTURE.md)
已知不确定性
| 项 | 说明 |
|---|---|
| 6.3 的影响面 | 已由 test/LAK.burn-params.test.js 在本地 Hardhat 网络实证复现;未在真实测试网上演练 |
| 手续费与销毁的长期经济影响 | 需建模,不在本报告范围 |
totalBurned 口径 |
6.2 建议为语义调整,是否实施由项目方决定 |
8. 行动建议
| 优先级 | 事项 | 依据 |
|---|---|---|
| ✅ 已完成 | 为 entertainmentIndex / musicIndex / burnSensitivity 增加上限校验(MAX_INDUSTRY_INDEX / MAX_BURN_SENSITIVITY)+ currentBurnBps 计算侧防御性封顶 |
6.3 |
| 中 | 对外表述中区分「协议销毁量(totalBurned)」与「总销毁量(MAX_SUPPLY − totalSupply())」 |
6.2 |
| 低 | 白皮书补充说明:手续费豁免同时免除行情销毁;豁免名单应最小化并公示 | 6.4 |
| 低 | 豁免名单、feeRecipient、销毁策略等治理动作纳入公开披露流程 |
6.4 |
| 低 | 精简 :301 的不可达分支(随主网版本一并处理) |
6.6 |
| ✅ 已完成 | 字节码与仓库源码一致性核验(⚠️ 仅对 2026-09-17 修复前的提交成立,见文首提示) | 第 2 节 |
⚠️ 上述建议中,凡涉及源码改动的,均须重新编译、重新部署、重新验证并重做多签交接(合约不可升级)。 建议与第三方审计的发现合并处理,一次性进入主网版本。
🔴 本条现已触发:6.3 的修复已于 2026-09-17 改动源码,因此必须走完 重新编译 → 重新部署 → 重新验证 → 重做多签交接。当前 Sepolia 部署仍是修复前版本、 与仓库源码不一致;下一次部署应把本修复与第三方审计的发现合并为同一个版本,避免改两次。
9. 免责声明
本报告为开发者导向的合约行为说明,由项目方使用链上公开数据与自有源码自行整理,不构成第三方安全审计、不构成安全认证、不构成形式化验证,也不构成任何投资建议或法律意见。
报告中的"确认行为"指有代码位置支撑的实现描述;"观察"与"不确定性"指基于阅读的推断,可能不完整或有误。在主网部署前,本合约仍须经独立第三方审计机构的完整审计。
报告日期:2026-09-16 · 审查对象:0xb25E69F0D1f07A4e473DAdf93Af5f9322AA5DBF6(Sepolia) · 源码:contracts/LAK.sol(310 行)