如何遍历匹配Alex&Happy生成的Solidity AST以构建代码分析规则引擎?
嘿,你已经搞定了最费劲的部分——用Alex和Happy搭好了Solidity的词法/语法分析器,还生成了AST!接下来要做的规则引擎,本质就是对AST做定向遍历和模式匹配,我来给你捋捋具体怎么搞:
不管啥规则,第一步得先搞懂你手里的AST每个节点到底对应Solidity里的啥语法元素。Happy生成的AST是Haskell数据类型,建议你先写个小工具把完整AST打印出来(用show函数就行),比如拿一段简单的Solidity代码(比如带函数调用的合约)生成AST,然后逐个节点拆解:
- 比如
FunctionDefinition节点会包含函数名、参数列表、函数体、修饰器这些字段 CallExpression节点会记录被调用的目标(比如是合约地址还是内置函数)和参数- 还有
ModifierInvocation、EventDefinition这些和Solidity特性强相关的节点
把这些节点类型和字段都理清楚,后续写规则的时候才知道该匹配啥。
遍历AST主要有两种路子,看你喜欢哪种:
1. 手动递归遍历(适合精准控制)
自己写递归函数,对每个节点类型做模式匹配,处理完当前节点再递归处理子节点。比如写个这样的Haskell函数:
data RuleViolation = RuleViolation { violationMsg :: String, sourceLoc :: SourceLocation } processAST :: ASTNode -> [RuleViolation] -- 处理函数定义节点,先检查函数相关规则,再遍历函数体 processAST (FunctionDefinition name params body modifiers loc) = let funcViolations = checkFunctionRules name modifiers loc bodyViolations = concatMap processAST body in funcViolations ++ bodyViolations -- 处理调用表达式,检查危险调用规则 processAST (CallExpression callee args loc) = checkDangerousCalls callee args loc ++ concatMap processAST args -- 其他节点直接递归处理子节点 processAST otherNode = concatMap processAST (getChildNodes otherNode)
这里的getChildNodes需要你自己实现,就是从每个AST节点里提取它的子节点列表(比如函数体里的语句、调用的参数都是子节点)。
2. 用Scrap Your Boilerplate(SYB)库减少重复代码
如果不想手动写每个节点的递归逻辑,可以用Haskell的Data.Generics库(SYB),它能自动遍历所有AST节点,帮你找到匹配特定类型的节点。比如找所有危险的空transfer调用:
import Data.Generics findEmptyTransferCalls :: AST -> [RuleViolation] findEmptyTransferCalls ast = everything (++) ([] `mkQ` checkEmptyTransfer) ast where checkEmptyTransfer :: CallExpression -> [RuleViolation] checkEmptyTransfer (CallExpression (Identifier "transfer") [] loc) = [RuleViolation "Empty transfer call has no effect" loc] checkEmptyTransfer _ = []
这个方式适合批量找某类节点,省得手动写大量递归代码。
规则引擎的核心就是一个个独立的检查规则,你可以把每个检查点封装成单独的函数,比如:
检查函数使用不当
比如检测没有加权限修饰器的public函数:
checkUnprotectedPublicFunctions :: FunctionDefinition -> [RuleViolation] checkUnprotectedPublicFunctions (FunctionDefinition name _ _ modifiers loc) | isPublicFunction name && not (hasAuthModifier modifiers) = [RuleViolation ("Public function " ++ name ++ " has no access control") loc] | otherwise = [] where isPublicFunction = ("public" `elem`) . functionVisibility hasAuthModifier = any (\(ModifierInvocation modName _) -> modName == "onlyOwner")
检查危险调用
比如检测低级别call/delegatecall没有检查返回值:
checkUncheckedLowLevelCalls :: CallExpression -> [RuleViolation] checkUncheckedLowLevelCalls callExpr@(CallExpression callee _ loc) | isLowLevelCall callee && not (isReturnChecked callExpr) = [RuleViolation "Low-level call does not check return value" loc] | otherwise = [] where isLowLevelCall (Identifier "call") = True isLowLevelCall (Identifier "delegatecall") = True isLowLevelCall _ = False -- 判断调用的返回值是否被检查(比如赋值给变量或用require判断) isReturnChecked = ... -- 这里需要根据AST结构实现逻辑
检查缺失特定元素
比如检测合约没有实现fallback函数:
checkMissingFallback :: ContractDefinition -> [RuleViolation] checkMissingFallback (ContractDefinition name functions _ loc) | not (any isFallbackFunction functions) = [RuleViolation ("Contract " ++ name ++ " missing fallback function") loc] | otherwise = [] where isFallbackFunction (FunctionDefinition "fallback" _ _ _ _) = True isFallbackFunction _ = False
很多规则需要上下文才能判断,比如“这个函数是不是在有继承的合约里?”“当前函数的可见性是啥?”这时候可以用Haskell的Reader或State monad来携带上下文,比如:
import Control.Monad.Reader type AnalysisContext = { currentContract :: ContractDefinition, currentFunction :: Maybe FunctionDefinition } processASTWithContext :: ASTNode -> Reader AnalysisContext [RuleViolation] processASTWithContext (FunctionDefinition name params body modifiers loc) = do ctx <- ask let newCtx = ctx { currentFunction = Just (FunctionDefinition name params body modifiers loc) } funcViolations <- checkFunctionRules newCtx loc bodyViolations <- local (const newCtx) (concatMapM processASTWithContext body) return (funcViolations ++ bodyViolations)
这样在检查规则的时候就能拿到当前所在的合约、函数这些上下文信息了。
先写一些带问题的Solidity测试用例,比如:
contract BadContract { function unsafeFunc() public { // 没有权限控制 } function doCall() { payable(address(0x123)).call{value: 1 ether}(""); // 没检查返回值 } }
生成AST后跑你的规则引擎,看能不能检测到这两个问题,然后逐步完善规则的逻辑,覆盖更多Solidity的语法特性(比如继承、修饰器、事件、枚举这些)。
内容的提问来源于stack exchange,提问作者Sheldor

