ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Foundry forge-lint 深度解析:`arbitrary-send-erc20-permit` 规则与 permit + transferFrom 的隐藏风险

Foundry forge-lint 深度解析:`arbitrary-send-erc20-permit` 规则与 permit + transferFrom 的隐藏风险 Foundry forge-lint 深度解析arbitrary-send-erc20-permit规则与 permit transferFrom 的隐藏风险【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry本篇基于 Foundry 的 lint 规则文档 arbitrary-send-erc20-permit.md 展开说明这条 High 级forge-lint规则检测的“permit之后跟任意from的transferFrom”模式为何不安全典型如 WETH 的 fallback 函数会把 permit 变成静默成功的空操作并逐条对照 规则实现源码 与 测试夹具 讲解其判定边界、排除条件与修复方式读完后你能准确判断告警是否成立、如何改写代码或在确认安全后正确抑制告警。规则元信息项目值规则 IDarbitrary-send-erc20-permit严重级别High诊断消息transferFrom uses an arbitrary from after permit实现文件crates/lint/src/sol/high/arbitrary_send_erc20.rs文档页面crates/lint/docs/arbitrary-send-erc20-permit.md测试夹具crates/lint/testdata/ArbitrarySendErc20Permit.sol / 期望输出在源码中该规则通过declare_forge_lint!宏注册注册处与基础的arbitrary-send-erc20规则共用同一个 late lint pass二者在 high/mod.rs 中一起登记。declare_forge_lint!( ARBITRARY_SEND_ERC20_PERMIT, Severity::High, arbitrary-send-erc20-permit, transferFrom uses an arbitrary from after permit );每条 lint 的文档页面都遵循统一模板 _template.md 定义的结构标题、Severity、ID、What it does / Why is this bad? / Example并由 CI 校验注册元数据与文档一致性详见 lint 文档目录说明。规则检测什么What it does文档给出的核心判定是在同一函数内先对同一 token、同一 owner 调用permit且 spender 为address(this)随后from未约束为msg.sender或address(this)的transferFrom/safeTransferFrom调用会被标记。文档还明确了三个边界条件覆盖面不仅识别裸的token.transferFrom(...)还包括常见的 SafeERC20、SafeTransferLib 封装成员形式token.safeTransferFrom(...)与库形式SafeERC20.safeTransferFrom(token, from, to, a)。关键立场permit的存在不会让任意from变安全即使 permit 的 value 与 transfer 金额一致——因为 permit 可能根本没有生效。排除项与 EIP-3156 闪电贷还款匹配的transferFromonFlashLoan之后从借款人处拉回amount fee到address(this)被排除在独立 helper 或 modifier 中签发的 permit 不会与后续 transfer 关联这类 transfer 会落到兄弟规则arbitrary-send-erc20名下。源码中对“sink汇点”的识别与文档描述一一对应见 match_sink接收方类型声明了 ERC20 签名的transferFrom(address,address,uint256) returns (bool)时transferFrom/safeTransferFrom3 参是 sink同名的 ERC721 重载无返回值被显式排除通过using SafeTransferLib for address附加到address上的 4 参safeTransferFrom(token, from, to, amount)也是 sink库形式Lib.safeTransferFrom(token, ...)仅在库声明了 4 参safeTransferFrom且 token 参数是addressSolady 风格或 ERC20 契约类型OpenZeppelinSafeERC20风格时匹配判定逻辑见 library_has_safe_transfer_from。为什么危险WETH 的 fallback 陷阱文档的Why is this bad?一节指出了这条规则存在的根本原因permittransferFrom是教科书式的 EIP-2612 流程看起来安全但当 token 本身没有实现permit却有 fallback 函数时经典例子就是 WETH会发生以下链条permit(...)被转发到 fallback静默成功但没有授权任何东西合约因为“信任了”这个空操作的 permit会把攻击者传入的任意from直接透传给transferFrom从而抽干任何用户先前授予给本合约的存量 allowance。文档给出的两条修复建议在部署时固定支持的 token并验证其正确实现了permit或者要求from msg.sender这样即使 permit 静默失效风险也只限于调用者自己的余额。文档还说明了一个合法的误报豁免场景如果你的代码独立证明了 permit 确实生效例如在 permit 前后读取token.nonces(owner)nonce 无变化就 revert或把 transfer 限制在受审的 token 白名单内人工复核后可用行内注释抑制// forge-lint: disable-next-line(arbitrary-send-erc20-permit) token.transferFrom(from, to, a);这一抑制方式在测试夹具中也有对应验证用例 okInlineDisablePermitLint。文档中的触发示例与推荐写法触发规则的最小示例继承自 原文档function pullWithPermit( address from, address to, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { token.permit(from, address(this), value, deadline, v, r, s); token.transferFrom(from, to, value); // arbitrary-send-erc20-permit }from完全由外部传入permit 的 owner 恰好也是这个frompermit 记录会“覆盖”该 sink于是报 permit 变体而非普通变体。推荐改写from固定为调用者function pullWithPermit( uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external { // from is implicitly the caller — permit transferFrom only touch the // callers own balance, even if token is a non-permit token with a fallback. token.permit(msg.sender, address(this), value, deadline, v, r, s); token.transferFrom(msg.sender, address(this), value); }注意即使token是带 fallback 的非 permit token如 WETH这个写法也只消耗调用者自己的余额不会波及他人 allowance。该写法对应夹具中的负例 okPermitMsgSender。源码级解析permit 如何与 sink 关联规则的核心是一个路径敏感path-sensitive的数据流分析器源码位于 arbitrary_send_erc20.rs。下面几个结构直接解释了文档中每条判定边界的实现依据。PermitRecord 与 TokenKey同 token、同 owner 的精确关联/// An EIP-2612 permit with spender address(this) seen earlier on the current path. struct PermitRecord { token: TokenKey, owner: VariableId, }定义处只有当permit调用的 spender 参数被证明是address(this)时才会记录这正是文档中“with this contract as the spender”的含义。TokenKeyL89-L101把 token 接收方规范化为“变量”或“结构体字段”两类键因此cfg.token.permit(...)之后的cfg.token.transferFrom(...)也能关联上对应用例 badPermitStructFieldToken。match_permit_call识别 permit 的两种形态match_permit_call 只接受两种调用形态token.permit(owner, spender, value, deadline, v, r, s)恰好 7 参OpenZeppelin 风格的库封装Lib.safePermit(token, owner, spender, ...)恰好 8 参且接收方是 library 契约。并且要求 spender 经is_self_expr证明为address(this)或其别名。别名识别会穿透payable(address(this))、address类型转换、两个分支都安全的三元表达式以及最多 3 层HELPER_DEPTH 3L47-L48的无参 helper 链如function _self() internal view returns (address) { return address(this); }。因此文档中“named-args 形式、别名 spender、payable(address(this))”等变体都计入 permit 记录对应用例 badPermitNamedArgs、badPermitSpenderAlias、badPermitPayableSelfSpender、badPermitHelperSelfSpender。判定分叉permit 记录只改变“报哪条规则”不消除告警sink 处的核心判定逻辑L618-L646值得特别注意} else if let Some(sink) match_sink(self.gcx, expr) !self.is_safe(sink.from) !self.consume_repayment(sink) { // A prior permit does not make the sink safe: a non-permit token with a // fallback (e.g. WETH) silently accepts the permit. let lint if self.permit_covers(sink) { ARBITRARY_SEND_ERC20_PERMIT } else { ARBITRARY_SEND_ERC20 }; self.hits.push((expr.span, lint)); }源码注释直接印证了文档的立场先前的 permit不会让 sink 变安全它只决定这条告警以arbitrary-send-erc20-permit还是arbitrary-send-erc20的身份报出。这也解释了文档中“permit 在独立 helper/modifier 中签发时不关联”的说法——只有分析器在当前路径上实际看到permit表达式才会入表。路径敏感状态分支、循环、try/catch、写失效State 维护四类事实证明安全的变量集合safe_vars、证明等于address(this)的变量集合self_vars、permit 记录表permits、以及待还款的闪电贷额度repayments。分析器对控制流做 meet 合并State::meet规则与文档边界一致两分支都 permit后汇合处的 sink 报一次交集保留 permit 记录对应用例 badPermitBothBranchesThenSink只有单分支 permit时汇合后 permit 事实被丢弃sink 回落为普通 lint对应用例 okPermitInOtherBranchtry/catch 隔离try 子句内建立的 permit 不泄漏到 try 之后okPermitInsideTryDoesNotLeak而 try 表达式本身成功时的状态会被成功子句继承badPermitTryCallSuccessSink变量重赋值即失效invalidate 在 token 或 owner 变量被写后丢弃对应 permit 记录delete token同样处理okPermitDeleteTokenKillsRecordmodifier 事实提升有严格边界只有当 modifier 前缀用require(param msg.sender | address(this))守卫、且该参数在 modifier 内未被改写时才会把安全事实提升到调用方实参hoist_modifier_facts。因此onlySelf(from)使 sink 不报okPermitModifierGuarded而会改写参数的rewriteSpender不能抑制告警okPermitModifierWritesParam。EIP-3156 还款排除的实现文档说“Matching EIP-3156 flash-loan repayments are excluded”对应 consume_repayment 与 match_flash_loan_call每次receiver.onFlashLoan(initiator, token, amount, fee, data)授权一次从receiver拉回amount fee顺序不限到address(this)的transferFrom每次消费一个额度。测试夹具覆盖了这些边界恰好一次还款不告警okPermitPlusFlashRepayment即使之前有覆盖(token, receiver)的 permit还款额度用完后第二次同形拉取报 permit 变体badPermitPlusFlashDoublePull两次onFlashLoan各授权一次还款两次拉取均合法okPermitDoubleFlashRepaymentamount fee的求和别名uint256 repayment amount fee;也视为匹配okPermitFlashRepaymentSumAlias还款目标为构造函数中初始化为address(this)的 immutable 同样识别okPermitFlashRepaymentToVault。完整走查测试夹具中的正例与负例全景ArbitrarySendErc20Permit.sol 用//compile-flags: --only-lint arbitrary-send-erc20-permit开启单规则检查共 30 个正例与 40 余个负例期望输出固化在 ArbitrarySendErc20Permit.stderr。按主题归纳应当告警permit 变体场景用例裸 permit 任意 from 的 transferFrombadPermitPlain成员形式token.safeTransferFrombadPermitSafeMember库形式SafeERC20.safeTransferFrom(token, ...)badPermitLibrary命名参数调用badPermitNamedArgs接口强相关名IERC20(rawToken)两侧通过底层 address 变量关联badPermitInterfaceCastSolady 风格token 为裸 address 的SafeTransferLib含using ... for address成员形式badPermitSoladyAddressToken、badPermitSoladyUsingForAddress两分支各自 permit、分支内 permit、unchecked 块、do-while 建立 permitbadPermitBothBranches 等owner 与 token 的本地别名双向badPermitOwnerAlias、badPermitTokenAlias两个 token 各一次 permit两个 sink 乱序执行——permit 记录不随使用而消耗badTwoPermitsTwoSinksDifferentOrderSafeERC20.safePermit库封装后再裸调 transferFrombadPermitSafeWrapperFollowedBySink嵌套 sink外层调用参数求值先于其状态写入副作用badPermitNestedSinkBeforeStateWrite不应告警负例的典型类别from可证明安全等于msg.sender/address(this)、require(from msg.sender)守卫、_msgSender()类 helper、两个分支都安全的三元表达式okPermitMsgSender、okPermitGuarded、okPermitTernaryBothSafe关联不上 permitspender 不是本合约okPermitWrongSpender、owner 与 sink 的from不同变量okPermitOwnerMismatch、token 不同okPermitWrongToken、sink 在 permit 之前okPermitAfterSink、permit 后 token/owner 被重赋值或deleteokPermitStateTokenReassigned 等、内部调用改写了 token 状态变量okPermitInternalCallSwitchesToken非目标形态ERC721 同名transferFrom/safeTransferFrom重载okErc721NotAffected、view/pure 函数整体跳过pass 入口处 L50-L63不可达代码return之后的死代码、前缀必退出的 modifierokModifierKillsBody。另外针对 internal 函数与 modifier 的参数分析器还做了一层调用点事实传播若某 internal 函数/修改器的每个调用点都传入可证明安全的实参其参数在函数体内直接视为安全seed_callsite_facts 与 callsite_indeximmutable/constant状态变量则从声明初始值与构造函数播种seed_immutable_facts。运行与配置单独运行该规则与测试夹具使用的编译参数一致forge lint --only-lint arbitrary-send-erc20-permit全量 lint该规则为 High 级默认在检查集中forge lint告警形态摘自 期望输出文件warning[arbitrary-send-erc20-permit]: transferFrom uses an arbitrary from after permit ╭▸ src/Vault.sol:134:9 │ 134 │ token.transferFrom(from, to, a); │ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ ╰ help: 参见本规则文档该规则没有额外的foundry.toml配置项文档页面不含 Configuration 一节按 模板约定 这表示无独立配置。逐行抑制使用// forge-lint: disable-next-line(arbitrary-send-erc20-permit)命名与诊断风格遵循 lint 编写风格指南。与arbitrary-send-erc20的关系与处置建议arbitrary-send-erc20-permit是 arbitrary-send-erc20 的精确化子规则两者的 sink 判定与安全事实共用同一套分析器区别只在“sink 前当前路径上是否存在同 token、同 owner、spender 为本合约的 permit 记录”。处置告警时默认按高危处理若 token 可能不是标准 EIP-2612 tokenWETH 即典型反例优先把from收敛为msg.sender或在部署时锁定并审计 token 的 permit 实现若已独立验证 permit 生效如 nonce 前后对比、token 白名单复核后按文档说明用行内注释抑制并注明理由若 permit 在 modifier/独立 helper 中签发本规则不会关联请转看arbitrary-send-erc20的告警与文档两者结论一致。小结arbitrary-send-erc20-permit抓住了一个容易被直觉掩盖的真实攻击面“permit 看起来成功了”不等于“permit 生效了”。文档给出了触发模式、WETH fallback 攻击链与两条修复路径实现源码 则通过路径敏感的状态表permit 记录、别名、安全事实、还款额度把文档中的每一条边界——SafeERC20/SafeTransferLib 封装、结构体字段 token、分支汇合、try/catch 隔离、变量写失效、EIP-3156 还款排除——都落成了可验证的逻辑而 测试夹具 以 30 正例与 40 负例固化了全部行为。理解这组材料既能让你在审计或开发中正确解读该规则也能在写 permit 类合约时从源头上规避任意from的资金风险。【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表