recon-generate 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer/call-tree-builder.d.ts +14 -0
- package/dist/analyzer/call-tree-builder.js +92 -0
- package/dist/analyzer/expression-analyzer.d.ts +14 -0
- package/dist/analyzer/expression-analyzer.js +176 -0
- package/dist/index.js +25 -4
- package/dist/pathsGenerator.d.ts +9 -0
- package/dist/pathsGenerator.js +627 -0
- package/dist/types.d.ts +11 -1
- package/dist/utils.d.ts +11 -1
- package/dist/utils.js +87 -0
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ContractDefinition, Expression, FunctionCall, FunctionDefinition, ModifierDefinition, ModifierInvocation, Statement } from 'solc-typed-ast';
|
|
2
|
+
import { CallTree } from '../types';
|
|
3
|
+
export interface CallTreeBuilderContext {
|
|
4
|
+
nodeCounter: {
|
|
5
|
+
value: number;
|
|
6
|
+
};
|
|
7
|
+
contract: ContractDefinition;
|
|
8
|
+
callStack: number[];
|
|
9
|
+
activeNodes: Map<number, number>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build a call tree for a function or modifier
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildCallTree(currentFunc: FunctionDefinition | ModifierDefinition, callArgs?: Expression[], fnCall?: FunctionCall | ModifierInvocation, callContext?: Statement, context?: CallTreeBuilderContext): CallTree;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildCallTree = buildCallTree;
|
|
4
|
+
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
5
|
+
const expression_analyzer_1 = require("./expression-analyzer");
|
|
6
|
+
/**
|
|
7
|
+
* Build a call tree for a function or modifier
|
|
8
|
+
*/
|
|
9
|
+
function buildCallTree(currentFunc, callArgs = [], fnCall, callContext, context) {
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('CallTreeBuilderContext is required');
|
|
12
|
+
}
|
|
13
|
+
const functionAstId = currentFunc.id;
|
|
14
|
+
const isRecursiveCall = context.callStack.includes(functionAstId);
|
|
15
|
+
const recursiveTargetNodeId = context.activeNodes.get(functionAstId);
|
|
16
|
+
const node = {
|
|
17
|
+
nodeId: context.nodeCounter.value++,
|
|
18
|
+
definition: currentFunc,
|
|
19
|
+
fnCall: fnCall,
|
|
20
|
+
callArgs: callArgs,
|
|
21
|
+
callContext: callContext,
|
|
22
|
+
children: [],
|
|
23
|
+
isRecursive: isRecursiveCall,
|
|
24
|
+
recursiveTargetNodeId: recursiveTargetNodeId
|
|
25
|
+
};
|
|
26
|
+
if (isRecursiveCall) {
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
context.callStack.push(functionAstId);
|
|
30
|
+
context.activeNodes.set(functionAstId, node.nodeId);
|
|
31
|
+
// For modifiers, process argument calls first (before modifier body)
|
|
32
|
+
if (currentFunc instanceof solc_typed_ast_1.ModifierDefinition && fnCall instanceof solc_typed_ast_1.ModifierInvocation) {
|
|
33
|
+
for (const arg of fnCall.vArguments) {
|
|
34
|
+
(0, expression_analyzer_1.collectCallsFromExpression)(arg, node.children, fnCall, undefined, (func, args, call, ctx) => buildCallTree(func, args, call, ctx, context), context.contract);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// For constructors, process argument calls first (before constructor body)
|
|
38
|
+
if (currentFunc instanceof solc_typed_ast_1.FunctionDefinition && currentFunc.isConstructor && fnCall instanceof solc_typed_ast_1.ModifierInvocation) {
|
|
39
|
+
for (const arg of fnCall.vArguments) {
|
|
40
|
+
(0, expression_analyzer_1.collectCallsFromExpression)(arg, node.children, fnCall, undefined, (func, args, call, ctx) => buildCallTree(func, args, call, ctx, context), context.contract);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Process function body first - only direct statements
|
|
44
|
+
if (currentFunc.vBody) {
|
|
45
|
+
// For modifiers, create the context for parameter resolution
|
|
46
|
+
const modifierContext = currentFunc instanceof solc_typed_ast_1.ModifierDefinition ?
|
|
47
|
+
{ modifier: currentFunc, resolvedArgs: callArgs } : undefined;
|
|
48
|
+
for (const stmt of currentFunc.vBody.vStatements) {
|
|
49
|
+
(0, expression_analyzer_1.collectCallsFromExpression)(stmt, node.children, stmt, modifierContext, (func, args, call, ctx) => buildCallTree(func, args, call, ctx, context), context.contract);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Helper function to resolve modifier arguments based on function parameters
|
|
53
|
+
function resolveModifierArguments(func, modifierArgs, functionCallArgs) {
|
|
54
|
+
return modifierArgs.map(arg => {
|
|
55
|
+
// If the argument is an Identifier that matches a function parameter, replace it with the actual argument
|
|
56
|
+
if (arg instanceof solc_typed_ast_1.Identifier) {
|
|
57
|
+
const paramIndex = func.vParameters.vParameters.findIndex(param => param.name === arg.name);
|
|
58
|
+
if (paramIndex !== -1 && paramIndex < functionCallArgs.length) {
|
|
59
|
+
return functionCallArgs[paramIndex];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return arg;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Process modifiers last for ALL functions (at the end of the frame in reverse order)
|
|
66
|
+
if (currentFunc instanceof solc_typed_ast_1.FunctionDefinition && currentFunc.vModifiers.length > 0) {
|
|
67
|
+
// Reverse the order of modifiers and add them at the end
|
|
68
|
+
const reversedModifiers = [...currentFunc.vModifiers].reverse();
|
|
69
|
+
for (const modifierInvocation of reversedModifiers) {
|
|
70
|
+
if (modifierInvocation.vModifierName.vReferencedDeclaration instanceof solc_typed_ast_1.ModifierDefinition) {
|
|
71
|
+
const calledModifier = modifierInvocation.vModifierName.vReferencedDeclaration;
|
|
72
|
+
// Resolve modifier arguments based on the actual function call arguments
|
|
73
|
+
const resolvedArgs = resolveModifierArguments(currentFunc, modifierInvocation.vArguments, callArgs);
|
|
74
|
+
node.children.push(buildCallTree(calledModifier, resolvedArgs, modifierInvocation, undefined, context));
|
|
75
|
+
}
|
|
76
|
+
else if (modifierInvocation.vModifierName.vReferencedDeclaration instanceof solc_typed_ast_1.ContractDefinition) {
|
|
77
|
+
// Handle constructor calls like ERC20("ETH", 18)
|
|
78
|
+
const baseContract = modifierInvocation.vModifierName.vReferencedDeclaration;
|
|
79
|
+
// Find the constructor in the base contract
|
|
80
|
+
const constructor = baseContract.vFunctions.find(f => f.isConstructor);
|
|
81
|
+
if (constructor) {
|
|
82
|
+
// Resolve constructor arguments as well
|
|
83
|
+
const resolvedArgs = resolveModifierArguments(currentFunc, modifierInvocation.vArguments, callArgs);
|
|
84
|
+
node.children.push(buildCallTree(constructor, resolvedArgs, modifierInvocation, undefined, context));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
context.callStack.pop();
|
|
90
|
+
context.activeNodes.delete(functionAstId);
|
|
91
|
+
return node;
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ASTNode, ContractDefinition, Expression, FunctionCall, FunctionDefinition, ModifierDefinition, Statement } from 'solc-typed-ast';
|
|
2
|
+
import { CallTree } from '../types';
|
|
3
|
+
export interface ModifierContext {
|
|
4
|
+
modifier: ModifierDefinition;
|
|
5
|
+
resolvedArgs: Expression[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Helper to resolve modifier parameters in expressions
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveModifierParams(expr: Expression, modifierContext?: ModifierContext): Expression;
|
|
11
|
+
/**
|
|
12
|
+
* Helper to collect direct function/library calls from an expression
|
|
13
|
+
*/
|
|
14
|
+
export declare function collectCallsFromExpression(expr: ASTNode, childrenArr: CallTree[], context: Statement, modifierContext?: ModifierContext, buildCallTree?: (currentFunc: FunctionDefinition | ModifierDefinition, callArgs: Expression[], fnCall?: FunctionCall, callContext?: Statement) => CallTree, contract?: ContractDefinition): void;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveModifierParams = resolveModifierParams;
|
|
4
|
+
exports.collectCallsFromExpression = collectCallsFromExpression;
|
|
5
|
+
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
6
|
+
const utils_1 = require("../utils");
|
|
7
|
+
const utils_2 = require("../utils");
|
|
8
|
+
const utils_3 = require("../utils");
|
|
9
|
+
/**
|
|
10
|
+
* Helper to resolve modifier parameters in expressions
|
|
11
|
+
*/
|
|
12
|
+
function resolveModifierParams(expr, modifierContext) {
|
|
13
|
+
if (!modifierContext)
|
|
14
|
+
return expr;
|
|
15
|
+
// If it's an identifier that matches a modifier parameter, replace it
|
|
16
|
+
if (expr instanceof solc_typed_ast_1.Identifier) {
|
|
17
|
+
const paramIndex = modifierContext.modifier.vParameters.vParameters.findIndex(param => param.name === expr.name);
|
|
18
|
+
if (paramIndex !== -1 && paramIndex < modifierContext.resolvedArgs.length) {
|
|
19
|
+
return modifierContext.resolvedArgs[paramIndex];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return expr;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Helper to collect direct function/library calls from an expression
|
|
26
|
+
*/
|
|
27
|
+
function collectCallsFromExpression(expr, childrenArr, context, modifierContext, buildCallTree, contract) {
|
|
28
|
+
if (!expr || !buildCallTree || !contract)
|
|
29
|
+
return;
|
|
30
|
+
// Find all function calls in this expression (include the root node itself if it is a call)
|
|
31
|
+
const rootCalls = [];
|
|
32
|
+
if (expr instanceof solc_typed_ast_1.FunctionCall) {
|
|
33
|
+
rootCalls.push(expr);
|
|
34
|
+
}
|
|
35
|
+
const allCalls = [...rootCalls, ...expr.getChildrenByType(solc_typed_ast_1.FunctionCall, true)].filter(c => !(0, utils_1.highLevelCall)(c) &&
|
|
36
|
+
!(0, utils_1.highLevelCallWithOptions)(c) &&
|
|
37
|
+
c.kind === solc_typed_ast_1.FunctionCallKind.FunctionCall &&
|
|
38
|
+
!!c.vReferencedDeclaration &&
|
|
39
|
+
(c.vReferencedDeclaration instanceof solc_typed_ast_1.FunctionDefinition || c.vReferencedDeclaration instanceof solc_typed_ast_1.ModifierDefinition));
|
|
40
|
+
// Deduplicate in case the root call also appears in children traversal
|
|
41
|
+
const uniqueCalls = Array.from(new Set(allCalls));
|
|
42
|
+
// Implement proper Solidity evaluation order
|
|
43
|
+
const evaluationOrder = [];
|
|
44
|
+
// Recursive function to traverse and collect calls in correct evaluation order
|
|
45
|
+
function collectInEvaluationOrder(node, visited = new Set()) {
|
|
46
|
+
if (!node)
|
|
47
|
+
return;
|
|
48
|
+
// If this is a function call we care about, process it
|
|
49
|
+
if (node instanceof solc_typed_ast_1.FunctionCall &&
|
|
50
|
+
!(0, utils_1.highLevelCall)(node) &&
|
|
51
|
+
!(0, utils_1.highLevelCallWithOptions)(node) &&
|
|
52
|
+
node.kind === solc_typed_ast_1.FunctionCallKind.FunctionCall &&
|
|
53
|
+
!!node.vReferencedDeclaration &&
|
|
54
|
+
(node.vReferencedDeclaration instanceof solc_typed_ast_1.FunctionDefinition || node.vReferencedDeclaration instanceof solc_typed_ast_1.ModifierDefinition) &&
|
|
55
|
+
uniqueCalls.includes(node) &&
|
|
56
|
+
!visited.has(node)) {
|
|
57
|
+
// First, evaluate all arguments (which may contain nested calls)
|
|
58
|
+
for (const arg of node.vArguments) {
|
|
59
|
+
collectInEvaluationOrder(arg, visited);
|
|
60
|
+
}
|
|
61
|
+
// Then add this call to the evaluation order
|
|
62
|
+
evaluationOrder.push(node);
|
|
63
|
+
visited.add(node);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// For binary operations, evaluate right operand first, then left operand (Solidity stack-based evaluation)
|
|
67
|
+
if (node.type === 'BinaryOperation') {
|
|
68
|
+
const binaryOp = node;
|
|
69
|
+
// Access left and right operands through children array
|
|
70
|
+
const left = binaryOp.children && binaryOp.children[0];
|
|
71
|
+
const right = binaryOp.children && binaryOp.children[1];
|
|
72
|
+
if (right)
|
|
73
|
+
collectInEvaluationOrder(right, visited);
|
|
74
|
+
if (left)
|
|
75
|
+
collectInEvaluationOrder(left, visited);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// For other nodes, recursively process children in order
|
|
79
|
+
const children = node.children || [];
|
|
80
|
+
for (const child of children) {
|
|
81
|
+
collectInEvaluationOrder(child, visited);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Start the evaluation order collection from the root expression
|
|
85
|
+
collectInEvaluationOrder(expr);
|
|
86
|
+
// Use the collected evaluation order
|
|
87
|
+
const sortedCalls = evaluationOrder.length > 0 ? evaluationOrder : uniqueCalls;
|
|
88
|
+
// Filter to get only direct calls while preserving evaluation order
|
|
89
|
+
const directCalls = sortedCalls.filter(call => {
|
|
90
|
+
// A call is direct if no other call in this expression contains it
|
|
91
|
+
return !uniqueCalls.some(otherCall => otherCall !== call &&
|
|
92
|
+
otherCall.getChildrenByType(solc_typed_ast_1.FunctionCall, true).includes(call));
|
|
93
|
+
});
|
|
94
|
+
for (const call of directCalls) {
|
|
95
|
+
let callNode = null;
|
|
96
|
+
// Library call via MemberAccess
|
|
97
|
+
if (call.vExpression instanceof solc_typed_ast_1.MemberAccess) {
|
|
98
|
+
const memberAccess = call.vExpression;
|
|
99
|
+
if (memberAccess.vReferencedDeclaration instanceof solc_typed_ast_1.FunctionDefinition) {
|
|
100
|
+
const libraryFunc = memberAccess.vReferencedDeclaration;
|
|
101
|
+
if (memberAccess.vExpression instanceof solc_typed_ast_1.Identifier) {
|
|
102
|
+
// Handle 'super' receiver first (special case)
|
|
103
|
+
if (memberAccess.vExpression.name === 'super') {
|
|
104
|
+
const currentContract = (0, utils_3.getEnclosingContract)(call);
|
|
105
|
+
if (currentContract) {
|
|
106
|
+
const targetFunc = (0, utils_3.resolveSuper)(contract, currentContract, (0, utils_2.getSignature)(libraryFunc));
|
|
107
|
+
if (targetFunc && targetFunc.implemented) {
|
|
108
|
+
const superArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
|
|
109
|
+
callNode = buildCallTree(targetFunc, superArgs, call, context);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Do NOT process further as library call
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const receiverDeclaration = memberAccess.vExpression.vReferencedDeclaration;
|
|
116
|
+
if (receiverDeclaration && receiverDeclaration instanceof solc_typed_ast_1.ContractDefinition) {
|
|
117
|
+
// Direct library call: HelperLibrary.increase(10)
|
|
118
|
+
if (libraryFunc.implemented) {
|
|
119
|
+
const resolvedLibraryArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
|
|
120
|
+
callNode = buildCallTree(libraryFunc, resolvedLibraryArgs, call, context);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// "Using for" call: result.increase()
|
|
125
|
+
if (libraryFunc.implemented) {
|
|
126
|
+
const resolvedReceiver = resolveModifierParams(memberAccess.vExpression, modifierContext);
|
|
127
|
+
const resolvedArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
|
|
128
|
+
const libraryArgs = [resolvedReceiver, ...resolvedArgs];
|
|
129
|
+
callNode = buildCallTree(libraryFunc, libraryArgs, call, context);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Complex receiver - collect receiver calls to add as children of this call
|
|
136
|
+
const receiverCalls = [];
|
|
137
|
+
collectCallsFromExpression(memberAccess.vExpression, receiverCalls, context, modifierContext, buildCallTree, contract);
|
|
138
|
+
// Then add the library call with receiver calls as children
|
|
139
|
+
if (libraryFunc.implemented) {
|
|
140
|
+
const resolvedReceiver = resolveModifierParams(memberAccess.vExpression, modifierContext);
|
|
141
|
+
const resolvedArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
|
|
142
|
+
const libraryArgs = [resolvedReceiver, ...resolvedArgs];
|
|
143
|
+
callNode = buildCallTree(libraryFunc, libraryArgs, call, context);
|
|
144
|
+
// Add receiver calls as children (before body calls)
|
|
145
|
+
if (callNode) {
|
|
146
|
+
callNode.children = [...receiverCalls, ...callNode.children];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Direct function call (could be internal/virtual and overridden)
|
|
154
|
+
const originalFunc = call.vReferencedDeclaration;
|
|
155
|
+
const resolved = (0, utils_3.resolveOverride)(contract, (0, utils_2.getSignature)(originalFunc)) || originalFunc;
|
|
156
|
+
if (resolved.implemented) {
|
|
157
|
+
// Resolve modifier parameters in call arguments
|
|
158
|
+
const resolvedCallArgs = call.vArguments.map((arg) => resolveModifierParams(arg, modifierContext));
|
|
159
|
+
callNode = buildCallTree(resolved, resolvedCallArgs, call, context);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// If we created a call node, process its arguments for nested calls
|
|
163
|
+
if (callNode) {
|
|
164
|
+
// First, recursively process arguments to find nested calls (these should come first)
|
|
165
|
+
const argumentCalls = [];
|
|
166
|
+
for (const arg of call.vArguments) {
|
|
167
|
+
collectCallsFromExpression(arg, argumentCalls, context, modifierContext, buildCallTree, contract);
|
|
168
|
+
}
|
|
169
|
+
// Then process function body calls (from buildCallTree)
|
|
170
|
+
const bodyCalls = callNode.children;
|
|
171
|
+
// Combine: argument calls first, then body calls
|
|
172
|
+
callNode.children = [...argumentCalls, ...bodyCalls];
|
|
173
|
+
childrenArr.push(callNode);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const case_1 = require("case");
|
|
40
40
|
const generator_1 = require("./generator");
|
|
41
41
|
const coverage_1 = require("./coverage");
|
|
42
|
+
const pathsGenerator_1 = require("./pathsGenerator");
|
|
42
43
|
const utils_1 = require("./utils");
|
|
43
44
|
const link_1 = require("./link");
|
|
44
45
|
function parseFilter(input) {
|
|
@@ -129,14 +130,32 @@ async function main() {
|
|
|
129
130
|
.option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
|
|
130
131
|
.option('--name <suite>', 'Suite name; affects coverage filename (recon-<name>-coverage.json)')
|
|
131
132
|
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
132
|
-
.action(async (opts) => {
|
|
133
|
+
.action(async (opts, cmd) => {
|
|
134
|
+
var _a;
|
|
133
135
|
const workspaceRoot = process.cwd();
|
|
134
136
|
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
135
137
|
const foundryRoot = path.dirname(foundryConfig);
|
|
136
|
-
const
|
|
138
|
+
const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
|
|
139
|
+
const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
|
|
137
140
|
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
138
141
|
await (0, coverage_1.runCoverage)(foundryRoot, foundryConfig, opts.cryticName || 'CryticTester', suiteSnake);
|
|
139
142
|
});
|
|
143
|
+
program
|
|
144
|
+
.command('paths')
|
|
145
|
+
.description('Generate recon-paths.json from a Crytic tester contract without scaffolding tests')
|
|
146
|
+
.option('--crytic-name <name>', 'Name of the Crytic tester contract to compile', 'CryticTester')
|
|
147
|
+
.option('--name <suite>', 'Suite name; affects paths filename (recon-<name>-paths.json)')
|
|
148
|
+
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
149
|
+
.action(async (opts, cmd) => {
|
|
150
|
+
var _a;
|
|
151
|
+
const workspaceRoot = process.cwd();
|
|
152
|
+
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
153
|
+
const foundryRoot = path.dirname(foundryConfig);
|
|
154
|
+
const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
|
|
155
|
+
const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
|
|
156
|
+
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
157
|
+
await (0, pathsGenerator_1.runPaths)(foundryRoot, opts.cryticName || 'CryticTester', suiteSnake);
|
|
158
|
+
});
|
|
140
159
|
program
|
|
141
160
|
.command('link')
|
|
142
161
|
.description('Link library addresses into echidna/medusa configs via crytic-compile')
|
|
@@ -144,11 +163,13 @@ async function main() {
|
|
|
144
163
|
.option('--medusa-config <path>', 'Path to medusa json (defaults based on --name)')
|
|
145
164
|
.option('--name <suite>', 'Suite name to pick config defaults (echidna-<name>.yaml / medusa-<name>.json)')
|
|
146
165
|
.option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
|
|
147
|
-
.action(async (opts) => {
|
|
166
|
+
.action(async (opts, cmd) => {
|
|
167
|
+
var _a;
|
|
148
168
|
const workspaceRoot = process.cwd();
|
|
149
169
|
const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
|
|
150
170
|
const foundryRoot = path.dirname(foundryConfig);
|
|
151
|
-
const
|
|
171
|
+
const parentOpts = ((_a = cmd.parent) === null || _a === void 0 ? void 0 : _a.opts()) || {};
|
|
172
|
+
const suiteRaw = opts.name || parentOpts.name ? String(opts.name || parentOpts.name).trim() : '';
|
|
152
173
|
const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
|
|
153
174
|
const suffix = suiteSnake ? `-${suiteSnake}` : '';
|
|
154
175
|
const echidnaDefault = `echidna${suffix}.yaml`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates minimal paths needed for 100% branch coverage.
|
|
5
|
+
* Output is optimized for LLM consumption.
|
|
6
|
+
*/
|
|
7
|
+
/** Simple output: function name -> array of path condition strings */
|
|
8
|
+
export type PathOutput = Record<string, string[]>;
|
|
9
|
+
export declare const runPaths: (foundryRoot: string, cryticName: string, suiteNameSnake?: string) => Promise<void>;
|
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Path Generator
|
|
4
|
+
*
|
|
5
|
+
* Generates minimal paths needed for 100% branch coverage.
|
|
6
|
+
* Output is optimized for LLM consumption.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.runPaths = void 0;
|
|
43
|
+
const child_process_1 = require("child_process");
|
|
44
|
+
const fs = __importStar(require("fs/promises"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
47
|
+
const call_tree_builder_1 = require("./analyzer/call-tree-builder");
|
|
48
|
+
const utils_1 = require("./utils");
|
|
49
|
+
// Contracts to skip (test helpers)
|
|
50
|
+
const SKIP_CONTRACTS = new Set(['IHevm', 'Vm', 'StdChains', 'StdCheatsSafe', 'StdCheats']);
|
|
51
|
+
// ==================== Helpers ====================
|
|
52
|
+
const runCmd = (cmd, cwd) => {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
(0, child_process_1.exec)(cmd, { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (err, _stdout, stderr) => {
|
|
55
|
+
if (err) {
|
|
56
|
+
reject(new Error(stderr || err.message));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
resolve();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const loadLatestSourceUnits = async (foundryRoot) => {
|
|
65
|
+
var _a;
|
|
66
|
+
const outDir = path.join(foundryRoot, '.recon', 'out');
|
|
67
|
+
const buildInfoDir = path.join(outDir, 'build-info');
|
|
68
|
+
let files = [];
|
|
69
|
+
try {
|
|
70
|
+
const entries = await fs.readdir(buildInfoDir);
|
|
71
|
+
const jsonFiles = entries.filter((f) => f.endsWith('.json'));
|
|
72
|
+
files = await Promise.all(jsonFiles.map(async (f) => ({
|
|
73
|
+
name: f,
|
|
74
|
+
path: path.join(buildInfoDir, f),
|
|
75
|
+
mtime: (await fs.stat(path.join(buildInfoDir, f))).mtime,
|
|
76
|
+
})));
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
throw new Error(`No build-info directory found at ${buildInfoDir}: ${e}`);
|
|
80
|
+
}
|
|
81
|
+
if (files.length === 0) {
|
|
82
|
+
throw new Error(`No build-info JSON files found in ${buildInfoDir}.`);
|
|
83
|
+
}
|
|
84
|
+
const latestFile = files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())[0].path;
|
|
85
|
+
const fileContent = await fs.readFile(latestFile, 'utf-8');
|
|
86
|
+
const buildInfo = JSON.parse(fileContent);
|
|
87
|
+
const buildOutput = (_a = buildInfo.output) !== null && _a !== void 0 ? _a : buildInfo;
|
|
88
|
+
if (!buildOutput) {
|
|
89
|
+
throw new Error(`Build-info file ${latestFile} is missing output data.`);
|
|
90
|
+
}
|
|
91
|
+
const filteredAstData = { ...buildOutput };
|
|
92
|
+
if (filteredAstData.sources) {
|
|
93
|
+
const validSources = {};
|
|
94
|
+
for (const [key, content] of Object.entries(filteredAstData.sources)) {
|
|
95
|
+
const ast = content.ast || content.legacyAST || content.AST;
|
|
96
|
+
if (ast && (ast.nodeType === 'SourceUnit' || ast.name === 'SourceUnit')) {
|
|
97
|
+
validSources[key] = content;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
filteredAstData.sources = validSources;
|
|
101
|
+
}
|
|
102
|
+
const reader = new solc_typed_ast_1.ASTReader();
|
|
103
|
+
return reader.read(filteredAstData);
|
|
104
|
+
};
|
|
105
|
+
// ==================== Main Entry ====================
|
|
106
|
+
const runPaths = async (foundryRoot, cryticName, suiteNameSnake) => {
|
|
107
|
+
const buildCmd = `forge build --contracts ${cryticName} --build-info --out .recon/out`.replace(/\s+/g, ' ').trim();
|
|
108
|
+
await runCmd(buildCmd, foundryRoot);
|
|
109
|
+
const sourceUnits = await loadLatestSourceUnits(foundryRoot);
|
|
110
|
+
if (!sourceUnits || sourceUnits.length === 0) {
|
|
111
|
+
throw new Error('No source units were produced from the Crytic build; cannot generate paths.');
|
|
112
|
+
}
|
|
113
|
+
// Find target contract
|
|
114
|
+
const targetContract = findContract(sourceUnits, cryticName);
|
|
115
|
+
if (!targetContract) {
|
|
116
|
+
throw new Error(`Contract ${cryticName} not found`);
|
|
117
|
+
}
|
|
118
|
+
// Get target functions (including inherited ones)
|
|
119
|
+
const allFunctions = (0, utils_1.getDefinitions)(targetContract, 'vFunctions');
|
|
120
|
+
const targetFunctions = allFunctions.filter(f => f.visibility === solc_typed_ast_1.FunctionVisibility.Public &&
|
|
121
|
+
f.implemented &&
|
|
122
|
+
!f.name.startsWith('_'));
|
|
123
|
+
// Process each function
|
|
124
|
+
const output = {};
|
|
125
|
+
let totalPaths = 0;
|
|
126
|
+
for (const func of targetFunctions) {
|
|
127
|
+
// Find external call
|
|
128
|
+
const externalCall = findExternalCall(func);
|
|
129
|
+
if (!externalCall)
|
|
130
|
+
continue;
|
|
131
|
+
// Build call tree for external function
|
|
132
|
+
const extFunc = findFunction(sourceUnits, externalCall.contract, externalCall.function);
|
|
133
|
+
if (!extFunc)
|
|
134
|
+
continue;
|
|
135
|
+
const extContract = findContract(sourceUnits, externalCall.contract);
|
|
136
|
+
if (!extContract)
|
|
137
|
+
continue;
|
|
138
|
+
// Build call tree context
|
|
139
|
+
const context = {
|
|
140
|
+
nodeCounter: { value: 0 },
|
|
141
|
+
contract: extContract,
|
|
142
|
+
callStack: [],
|
|
143
|
+
activeNodes: new Map()
|
|
144
|
+
};
|
|
145
|
+
const callTree = (0, call_tree_builder_1.buildCallTree)(extFunc, [], undefined, undefined, context);
|
|
146
|
+
// Build param mapping
|
|
147
|
+
const paramMapping = buildParamMapping(func, externalCall);
|
|
148
|
+
// Enumerate paths
|
|
149
|
+
const enumerator = new PathEnumerator(sourceUnits, paramMapping);
|
|
150
|
+
const paths = enumerator.enumerate(callTree);
|
|
151
|
+
if (paths.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
// Simple output - combined string
|
|
154
|
+
const simplified = simplifyPaths(paths);
|
|
155
|
+
// Skip if no real branches (only "true")
|
|
156
|
+
if (simplified.length === 1 && simplified[0] === 'true')
|
|
157
|
+
continue;
|
|
158
|
+
output[func.name] = simplified;
|
|
159
|
+
totalPaths += simplified.length;
|
|
160
|
+
}
|
|
161
|
+
const pathsName = suiteNameSnake ? `recon-${suiteNameSnake}-paths.json` : 'recon-paths.json';
|
|
162
|
+
const pathsFilePath = path.join(foundryRoot, pathsName);
|
|
163
|
+
await fs.writeFile(pathsFilePath, JSON.stringify(output, null, 2));
|
|
164
|
+
console.log(`[recon-generate] Wrote paths file to ${pathsFilePath}`);
|
|
165
|
+
};
|
|
166
|
+
exports.runPaths = runPaths;
|
|
167
|
+
// ==================== Source Helpers ====================
|
|
168
|
+
function findContract(units, name) {
|
|
169
|
+
for (const unit of units) {
|
|
170
|
+
for (const contract of unit.vContracts) {
|
|
171
|
+
if (contract.name === name)
|
|
172
|
+
return contract;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function findFunction(units, contractName, funcName) {
|
|
178
|
+
const contract = findContract(units, contractName);
|
|
179
|
+
if (!contract)
|
|
180
|
+
return null;
|
|
181
|
+
for (const func of contract.vFunctions) {
|
|
182
|
+
if (func.name === funcName && func.implemented)
|
|
183
|
+
return func;
|
|
184
|
+
}
|
|
185
|
+
// Check base contracts
|
|
186
|
+
for (const baseContract of contract.vLinearizedBaseContracts) {
|
|
187
|
+
if (baseContract.id === contract.id)
|
|
188
|
+
continue;
|
|
189
|
+
for (const f of baseContract.vFunctions) {
|
|
190
|
+
if (f.name === funcName && f.implemented)
|
|
191
|
+
return f;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
function findExternalCall(func) {
|
|
197
|
+
if (!func.vBody)
|
|
198
|
+
return null;
|
|
199
|
+
const calls = func.vBody.getChildrenByType(solc_typed_ast_1.FunctionCall);
|
|
200
|
+
for (const call of calls) {
|
|
201
|
+
const expr = call.vExpression;
|
|
202
|
+
if (expr instanceof solc_typed_ast_1.MemberAccess) {
|
|
203
|
+
const baseType = expr.vExpression.typeString || '';
|
|
204
|
+
if (baseType.startsWith('contract ')) {
|
|
205
|
+
const match = baseType.match(/contract\s+(\w+)/);
|
|
206
|
+
if (match && !SKIP_CONTRACTS.has(match[1])) {
|
|
207
|
+
return {
|
|
208
|
+
contract: match[1],
|
|
209
|
+
function: expr.memberName,
|
|
210
|
+
args: call.vArguments
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function buildParamMapping(func, extCall) {
|
|
219
|
+
const mapping = new Map();
|
|
220
|
+
for (const param of func.vParameters.vParameters) {
|
|
221
|
+
mapping.set(param.name, param.name);
|
|
222
|
+
}
|
|
223
|
+
return mapping;
|
|
224
|
+
}
|
|
225
|
+
// ==================== Path Enumeration ====================
|
|
226
|
+
class PathEnumerator {
|
|
227
|
+
constructor(sourceUnits, paramMap) {
|
|
228
|
+
this.pathIdCounter = 0;
|
|
229
|
+
this.branchPoints = 0;
|
|
230
|
+
this.sourceUnits = sourceUnits;
|
|
231
|
+
this.paramMap = new Map(paramMap);
|
|
232
|
+
}
|
|
233
|
+
enumerate(callTree) {
|
|
234
|
+
this.pathIdCounter = 0;
|
|
235
|
+
this.branchPoints = 0;
|
|
236
|
+
const paths = this.processCallTree(callTree);
|
|
237
|
+
return paths
|
|
238
|
+
.filter(p => p.result === 'success')
|
|
239
|
+
.map(p => ({ id: p.id, conditions: p.conditions, requires: p.requires, result: p.result }));
|
|
240
|
+
}
|
|
241
|
+
processCallTree(node) {
|
|
242
|
+
const def = node.definition;
|
|
243
|
+
// Process children first (leaf to root)
|
|
244
|
+
const childPathsMap = new Map();
|
|
245
|
+
for (const child of node.children) {
|
|
246
|
+
if (child.fnCall instanceof solc_typed_ast_1.ModifierInvocation)
|
|
247
|
+
continue;
|
|
248
|
+
const savedMap = new Map(this.paramMap);
|
|
249
|
+
this.updateParamMap(child);
|
|
250
|
+
const childPaths = this.processCallTree(child);
|
|
251
|
+
childPathsMap.set(child.nodeId, childPaths);
|
|
252
|
+
this.paramMap = savedMap;
|
|
253
|
+
}
|
|
254
|
+
// Process function body
|
|
255
|
+
let activePaths = [{
|
|
256
|
+
id: ++this.pathIdCounter,
|
|
257
|
+
conditions: [],
|
|
258
|
+
requires: [],
|
|
259
|
+
terminated: false
|
|
260
|
+
}];
|
|
261
|
+
if (def.vBody) {
|
|
262
|
+
activePaths = this.walkBlock(def.vBody, activePaths, node, childPathsMap);
|
|
263
|
+
}
|
|
264
|
+
// Mark non-terminated as success
|
|
265
|
+
for (const p of activePaths) {
|
|
266
|
+
if (!p.terminated) {
|
|
267
|
+
p.terminated = true;
|
|
268
|
+
p.result = 'success';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return activePaths;
|
|
272
|
+
}
|
|
273
|
+
updateParamMap(node) {
|
|
274
|
+
const params = node.definition.vParameters.vParameters;
|
|
275
|
+
const args = node.callArgs;
|
|
276
|
+
for (let i = 0; i < params.length && i < args.length; i++) {
|
|
277
|
+
if (params[i].name && args[i]) {
|
|
278
|
+
this.paramMap.set(params[i].name, this.resolveExpr(args[i]));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
walkBlock(block, paths, node, childPaths) {
|
|
283
|
+
for (const stmt of block.vStatements) {
|
|
284
|
+
if (paths.every(p => p.terminated))
|
|
285
|
+
break;
|
|
286
|
+
paths = this.processStatement(stmt, paths, node, childPaths);
|
|
287
|
+
}
|
|
288
|
+
return paths;
|
|
289
|
+
}
|
|
290
|
+
processStatement(stmt, paths, node, childPaths) {
|
|
291
|
+
if (stmt instanceof solc_typed_ast_1.IfStatement) {
|
|
292
|
+
return this.handleIf(stmt, paths, node, childPaths);
|
|
293
|
+
}
|
|
294
|
+
if (stmt instanceof solc_typed_ast_1.TryStatement) {
|
|
295
|
+
return this.handleTryCatch(stmt, paths, node, childPaths);
|
|
296
|
+
}
|
|
297
|
+
if (stmt instanceof solc_typed_ast_1.Return) {
|
|
298
|
+
for (const p of paths)
|
|
299
|
+
if (!p.terminated) {
|
|
300
|
+
p.terminated = true;
|
|
301
|
+
p.result = 'success';
|
|
302
|
+
}
|
|
303
|
+
return paths;
|
|
304
|
+
}
|
|
305
|
+
if (stmt instanceof solc_typed_ast_1.RevertStatement) {
|
|
306
|
+
for (const p of paths)
|
|
307
|
+
if (!p.terminated) {
|
|
308
|
+
p.terminated = true;
|
|
309
|
+
p.result = 'revert';
|
|
310
|
+
}
|
|
311
|
+
return paths;
|
|
312
|
+
}
|
|
313
|
+
if (stmt instanceof solc_typed_ast_1.Block || stmt instanceof solc_typed_ast_1.UncheckedBlock) {
|
|
314
|
+
return this.walkBlock(stmt, paths, node, childPaths);
|
|
315
|
+
}
|
|
316
|
+
// Check for require/assert
|
|
317
|
+
paths = this.checkRequire(stmt, paths);
|
|
318
|
+
// Check for external calls (add as constraint - must succeed)
|
|
319
|
+
paths = this.checkExternalCalls(stmt, paths);
|
|
320
|
+
// Check for internal calls
|
|
321
|
+
paths = this.checkInternalCalls(stmt, paths, node, childPaths);
|
|
322
|
+
return paths;
|
|
323
|
+
}
|
|
324
|
+
handleTryCatch(stmt, paths, node, childPaths) {
|
|
325
|
+
this.branchPoints++;
|
|
326
|
+
// Get the external call expression
|
|
327
|
+
const extCall = stmt.vExternalCall;
|
|
328
|
+
const callExpr = this.formatExternalCall(extCall);
|
|
329
|
+
const results = [];
|
|
330
|
+
for (const path of paths) {
|
|
331
|
+
if (path.terminated) {
|
|
332
|
+
results.push(path);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Process each clause
|
|
336
|
+
for (const clause of stmt.vClauses) {
|
|
337
|
+
if (clause.errorName === '') {
|
|
338
|
+
// Success clause (try block) - external call succeeds
|
|
339
|
+
const successPath = {
|
|
340
|
+
id: ++this.pathIdCounter,
|
|
341
|
+
conditions: [...path.conditions, {
|
|
342
|
+
original: callExpr,
|
|
343
|
+
resolved: callExpr,
|
|
344
|
+
mustBeTrue: true
|
|
345
|
+
}],
|
|
346
|
+
requires: [...path.requires],
|
|
347
|
+
terminated: false
|
|
348
|
+
};
|
|
349
|
+
const successResults = this.walkBlock(clause.vBlock, [successPath], node, childPaths);
|
|
350
|
+
results.push(...successResults);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
// Catch clause - external call fails
|
|
354
|
+
const catchPath = {
|
|
355
|
+
id: ++this.pathIdCounter,
|
|
356
|
+
conditions: [...path.conditions, {
|
|
357
|
+
original: callExpr,
|
|
358
|
+
resolved: callExpr,
|
|
359
|
+
mustBeTrue: false // negated - call failed
|
|
360
|
+
}],
|
|
361
|
+
requires: [...path.requires],
|
|
362
|
+
terminated: false
|
|
363
|
+
};
|
|
364
|
+
const catchResults = this.walkBlock(clause.vBlock, [catchPath], node, childPaths);
|
|
365
|
+
results.push(...catchResults);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return results;
|
|
370
|
+
}
|
|
371
|
+
checkExternalCalls(stmt, paths) {
|
|
372
|
+
var _a, _b;
|
|
373
|
+
// Find all function calls in this statement
|
|
374
|
+
const calls = ((_b = (_a = stmt).getChildrenByType) === null || _b === void 0 ? void 0 : _b.call(_a, solc_typed_ast_1.FunctionCall)) || [];
|
|
375
|
+
for (const call of calls) {
|
|
376
|
+
// Check if it's a high-level external call
|
|
377
|
+
if ((0, utils_1.highLevelCall)(call)) {
|
|
378
|
+
const callExpr = this.formatExternalCall(call);
|
|
379
|
+
// Add as a constraint - external call must succeed
|
|
380
|
+
for (const p of paths) {
|
|
381
|
+
if (!p.terminated) {
|
|
382
|
+
p.requires.push({
|
|
383
|
+
original: callExpr,
|
|
384
|
+
resolved: callExpr,
|
|
385
|
+
mustBeTrue: true
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return paths;
|
|
392
|
+
}
|
|
393
|
+
formatExternalCall(call) {
|
|
394
|
+
if (call.vExpression instanceof solc_typed_ast_1.MemberAccess) {
|
|
395
|
+
const base = call.vExpression.vExpression;
|
|
396
|
+
const func = call.vExpression.memberName;
|
|
397
|
+
const baseStr = this.resolveExpr(base);
|
|
398
|
+
const args = call.vArguments.map(a => this.resolveExpr(a)).join(', ');
|
|
399
|
+
return `${baseStr}.${func}(${args})`;
|
|
400
|
+
}
|
|
401
|
+
return this.safeToSource(call);
|
|
402
|
+
}
|
|
403
|
+
handleIf(stmt, paths, node, childPaths) {
|
|
404
|
+
this.branchPoints++;
|
|
405
|
+
const condOrig = this.safeToSource(stmt.vCondition);
|
|
406
|
+
const condResolved = this.resolveExpr(stmt.vCondition);
|
|
407
|
+
const results = [];
|
|
408
|
+
for (const path of paths) {
|
|
409
|
+
if (path.terminated) {
|
|
410
|
+
results.push(path);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
// True branch
|
|
414
|
+
const truePath = {
|
|
415
|
+
id: ++this.pathIdCounter,
|
|
416
|
+
conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: true }],
|
|
417
|
+
requires: [...path.requires],
|
|
418
|
+
terminated: false
|
|
419
|
+
};
|
|
420
|
+
if (stmt.vTrueBody) {
|
|
421
|
+
const trueResults = stmt.vTrueBody instanceof solc_typed_ast_1.Block || stmt.vTrueBody instanceof solc_typed_ast_1.UncheckedBlock
|
|
422
|
+
? this.walkBlock(stmt.vTrueBody, [truePath], node, childPaths)
|
|
423
|
+
: this.processStatement(stmt.vTrueBody, [truePath], node, childPaths);
|
|
424
|
+
results.push(...trueResults);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
results.push(truePath);
|
|
428
|
+
}
|
|
429
|
+
// False branch
|
|
430
|
+
const falsePath = {
|
|
431
|
+
id: ++this.pathIdCounter,
|
|
432
|
+
conditions: [...path.conditions, { original: condOrig, resolved: condResolved, mustBeTrue: false }],
|
|
433
|
+
requires: [...path.requires],
|
|
434
|
+
terminated: false
|
|
435
|
+
};
|
|
436
|
+
if (stmt.vFalseBody) {
|
|
437
|
+
const falseResults = stmt.vFalseBody instanceof solc_typed_ast_1.Block || stmt.vFalseBody instanceof solc_typed_ast_1.UncheckedBlock
|
|
438
|
+
? this.walkBlock(stmt.vFalseBody, [falsePath], node, childPaths)
|
|
439
|
+
: this.processStatement(stmt.vFalseBody, [falsePath], node, childPaths);
|
|
440
|
+
results.push(...falseResults);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
results.push(falsePath);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return results;
|
|
447
|
+
}
|
|
448
|
+
checkRequire(stmt, paths) {
|
|
449
|
+
var _a, _b;
|
|
450
|
+
const calls = ((_b = (_a = stmt).getChildrenByType) === null || _b === void 0 ? void 0 : _b.call(_a, solc_typed_ast_1.FunctionCall)) || [];
|
|
451
|
+
for (const call of calls) {
|
|
452
|
+
const expr = call.vExpression;
|
|
453
|
+
if (expr instanceof solc_typed_ast_1.Identifier && (expr.name === 'require' || expr.name === 'assert')) {
|
|
454
|
+
if (call.vArguments.length > 0) {
|
|
455
|
+
const cond = call.vArguments[0];
|
|
456
|
+
const condOrig = this.safeToSource(cond);
|
|
457
|
+
const condResolved = this.resolveExpr(cond);
|
|
458
|
+
for (const p of paths) {
|
|
459
|
+
if (!p.terminated) {
|
|
460
|
+
p.requires.push({ original: condOrig, resolved: condResolved, mustBeTrue: true });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return paths;
|
|
467
|
+
}
|
|
468
|
+
checkInternalCalls(stmt, paths, node, childPaths) {
|
|
469
|
+
var _a, _b;
|
|
470
|
+
const calls = ((_b = (_a = stmt).getChildrenByType) === null || _b === void 0 ? void 0 : _b.call(_a, solc_typed_ast_1.FunctionCall)) || [];
|
|
471
|
+
for (const call of calls) {
|
|
472
|
+
for (const child of node.children) {
|
|
473
|
+
if (child.fnCall instanceof solc_typed_ast_1.ModifierInvocation)
|
|
474
|
+
continue;
|
|
475
|
+
if (child.fnCall.id === call.id && childPaths.has(child.nodeId)) {
|
|
476
|
+
const cPaths = childPaths.get(child.nodeId);
|
|
477
|
+
const merged = [];
|
|
478
|
+
for (const p of paths) {
|
|
479
|
+
if (p.terminated) {
|
|
480
|
+
merged.push(p);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
for (const cp of cPaths) {
|
|
484
|
+
if (cp.result === 'revert')
|
|
485
|
+
continue;
|
|
486
|
+
merged.push({
|
|
487
|
+
id: ++this.pathIdCounter,
|
|
488
|
+
conditions: [...p.conditions, ...cp.conditions],
|
|
489
|
+
requires: [...p.requires, ...cp.requires],
|
|
490
|
+
terminated: false
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (merged.length > 0)
|
|
495
|
+
paths = merged;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return paths;
|
|
500
|
+
}
|
|
501
|
+
resolveExpr(expr) {
|
|
502
|
+
if (expr instanceof solc_typed_ast_1.Literal)
|
|
503
|
+
return expr.value || '0';
|
|
504
|
+
if (expr instanceof solc_typed_ast_1.Identifier) {
|
|
505
|
+
const name = expr.name;
|
|
506
|
+
if (this.paramMap.has(name))
|
|
507
|
+
return this.paramMap.get(name);
|
|
508
|
+
// State variables - no prefix needed, context makes it clear
|
|
509
|
+
return name;
|
|
510
|
+
}
|
|
511
|
+
if (expr instanceof solc_typed_ast_1.MemberAccess) {
|
|
512
|
+
const base = expr.vExpression;
|
|
513
|
+
if (base instanceof solc_typed_ast_1.Identifier && ['msg', 'block', 'tx'].includes(base.name)) {
|
|
514
|
+
return `${base.name}.${expr.memberName}`;
|
|
515
|
+
}
|
|
516
|
+
return `${this.resolveExpr(base)}.${expr.memberName}`;
|
|
517
|
+
}
|
|
518
|
+
if (expr instanceof solc_typed_ast_1.IndexAccess) {
|
|
519
|
+
const base = this.resolveExpr(expr.vBaseExpression);
|
|
520
|
+
const idx = expr.vIndexExpression ? this.resolveExpr(expr.vIndexExpression) : '0';
|
|
521
|
+
return `${base}[${idx}]`;
|
|
522
|
+
}
|
|
523
|
+
if (expr instanceof solc_typed_ast_1.BinaryOperation) {
|
|
524
|
+
const left = this.resolveExpr(expr.vLeftExpression);
|
|
525
|
+
const right = this.resolveExpr(expr.vRightExpression);
|
|
526
|
+
return `(${left} ${expr.operator} ${right})`;
|
|
527
|
+
}
|
|
528
|
+
if (expr instanceof solc_typed_ast_1.UnaryOperation) {
|
|
529
|
+
const sub = this.resolveExpr(expr.vSubExpression);
|
|
530
|
+
return expr.prefix ? `${expr.operator}${sub}` : `${sub}${expr.operator}`;
|
|
531
|
+
}
|
|
532
|
+
if (expr instanceof solc_typed_ast_1.FunctionCall && expr.kind === solc_typed_ast_1.FunctionCallKind.TypeConversion && expr.vArguments.length > 0) {
|
|
533
|
+
return this.resolveExpr(expr.vArguments[0]);
|
|
534
|
+
}
|
|
535
|
+
return this.safeToSource(expr);
|
|
536
|
+
}
|
|
537
|
+
safeToSource(node) {
|
|
538
|
+
try {
|
|
539
|
+
return (0, utils_1.toSource)(node);
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return String(node);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// ==================== Path Simplification ====================
|
|
547
|
+
function simplifyPaths(paths) {
|
|
548
|
+
if (paths.length === 0)
|
|
549
|
+
return [];
|
|
550
|
+
// Extract unique branches
|
|
551
|
+
const branches = new Map();
|
|
552
|
+
for (const p of paths) {
|
|
553
|
+
for (const c of p.conditions) {
|
|
554
|
+
branches.set(c.resolved, true);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Find minimum covering set (greedy)
|
|
558
|
+
const needToCover = new Set();
|
|
559
|
+
for (const key of branches.keys()) {
|
|
560
|
+
needToCover.add(`T:${key}`);
|
|
561
|
+
needToCover.add(`F:${key}`);
|
|
562
|
+
}
|
|
563
|
+
const selected = [];
|
|
564
|
+
const covered = new Set();
|
|
565
|
+
while (covered.size < needToCover.size) {
|
|
566
|
+
let bestPath = null;
|
|
567
|
+
let bestNewCoverage = 0;
|
|
568
|
+
for (const path of paths) {
|
|
569
|
+
if (selected.includes(path))
|
|
570
|
+
continue;
|
|
571
|
+
let newCoverage = 0;
|
|
572
|
+
for (const c of path.conditions) {
|
|
573
|
+
const key = c.mustBeTrue ? `T:${c.resolved}` : `F:${c.resolved}`;
|
|
574
|
+
if (needToCover.has(key) && !covered.has(key))
|
|
575
|
+
newCoverage++;
|
|
576
|
+
}
|
|
577
|
+
if (newCoverage > bestNewCoverage) {
|
|
578
|
+
bestNewCoverage = newCoverage;
|
|
579
|
+
bestPath = path;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (!bestPath || bestNewCoverage === 0)
|
|
583
|
+
break;
|
|
584
|
+
selected.push(bestPath);
|
|
585
|
+
for (const c of bestPath.conditions) {
|
|
586
|
+
covered.add(c.mustBeTrue ? `T:${c.resolved}` : `F:${c.resolved}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (selected.length === 0)
|
|
590
|
+
selected.push(paths[0]);
|
|
591
|
+
// Format as strings - include both conditions and external call requires
|
|
592
|
+
return selected.map(p => {
|
|
593
|
+
const conds = p.conditions.map(c => {
|
|
594
|
+
const clean = c.resolved.replace(/storage\./g, '').replace(/^\((.+)\)$/, '$1');
|
|
595
|
+
return c.mustBeTrue ? clean : negateCond(clean);
|
|
596
|
+
});
|
|
597
|
+
// Add external call requires (detect by pattern: has dot and parentheses)
|
|
598
|
+
// Filter out SafeCast overflow checks (noise)
|
|
599
|
+
const extCalls = p.requires
|
|
600
|
+
.filter(r => r.resolved.includes('.') && r.resolved.includes('('))
|
|
601
|
+
.filter(r => !isOverflowCheck(r.resolved))
|
|
602
|
+
.map(r => r.mustBeTrue ? r.resolved : `!${r.resolved}`);
|
|
603
|
+
const allConds = [...conds, ...extCalls];
|
|
604
|
+
return allConds.join(' && ') || 'true';
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
// Detect SafeCast overflow guards like (x <= type(uint128).max)
|
|
608
|
+
function isOverflowCheck(s) {
|
|
609
|
+
return /<=\s*type\(u?int\d+\)\.max/.test(s);
|
|
610
|
+
}
|
|
611
|
+
function negateCond(cond) {
|
|
612
|
+
if (cond.startsWith('!'))
|
|
613
|
+
return cond.slice(1);
|
|
614
|
+
if (cond.includes('=='))
|
|
615
|
+
return cond.replace('==', '!=');
|
|
616
|
+
if (cond.includes('!='))
|
|
617
|
+
return cond.replace('!=', '==');
|
|
618
|
+
if (cond.includes('>='))
|
|
619
|
+
return cond.replace('>=', '<');
|
|
620
|
+
if (cond.includes('<='))
|
|
621
|
+
return cond.replace('<=', '>');
|
|
622
|
+
if (cond.includes('>') && !cond.includes('>='))
|
|
623
|
+
return cond.replace('>', '<=');
|
|
624
|
+
if (cond.includes('<') && !cond.includes('<='))
|
|
625
|
+
return cond.replace('<', '>=');
|
|
626
|
+
return `!(${cond})`;
|
|
627
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ASTNode } from "solc-typed-ast";
|
|
1
|
+
import { ASTNode, Expression, FunctionCall, FunctionDefinition, ModifierDefinition, ModifierInvocation, Statement } from "solc-typed-ast";
|
|
2
2
|
export declare enum Actor {
|
|
3
3
|
ACTOR = "actor",
|
|
4
4
|
ADMIN = "admin"
|
|
@@ -54,3 +54,13 @@ export type RecordItem = {
|
|
|
54
54
|
children: RecordItem[];
|
|
55
55
|
callType?: CallType;
|
|
56
56
|
};
|
|
57
|
+
export type CallTree = {
|
|
58
|
+
nodeId: number;
|
|
59
|
+
definition: FunctionDefinition | ModifierDefinition;
|
|
60
|
+
fnCall?: FunctionCall | ModifierInvocation;
|
|
61
|
+
callArgs: Expression[];
|
|
62
|
+
callContext?: Statement;
|
|
63
|
+
children: CallTree[];
|
|
64
|
+
isRecursive?: boolean;
|
|
65
|
+
recursiveTargetNodeId?: number;
|
|
66
|
+
};
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration } from 'solc-typed-ast';
|
|
1
|
+
import { ASTNode, ContractDefinition, Assignment, FunctionCall, FunctionDefinition, VariableDeclaration, EventDefinition, ErrorDefinition } from 'solc-typed-ast';
|
|
2
2
|
import { CallType } from './types';
|
|
3
3
|
export declare function fileExists(p: string): Promise<boolean>;
|
|
4
4
|
export declare function getFoundryConfigPath(workspaceRoot: string, override?: string): string;
|
|
@@ -29,3 +29,13 @@ export declare function toSource(node: ASTNode, version?: string): string;
|
|
|
29
29
|
export declare function getCallType(fnCall: FunctionCall): CallType;
|
|
30
30
|
export declare const getFunctionName: (fnDef: FunctionDefinition) => string;
|
|
31
31
|
export declare const signatureEquals: (a: FunctionDefinition, b: FunctionDefinition) => boolean;
|
|
32
|
+
export declare function getSignature(definition: FunctionDefinition | EventDefinition | ErrorDefinition): string;
|
|
33
|
+
export declare function getEnclosingContract(node: ASTNode): ContractDefinition | undefined;
|
|
34
|
+
export declare function resolveOverride(root: ContractDefinition, signature: string): FunctionDefinition | undefined;
|
|
35
|
+
export declare function idToContract(root: ContractDefinition, id: number): ContractDefinition | undefined;
|
|
36
|
+
export declare function forwardLinearization(root: ContractDefinition): ContractDefinition[];
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the `super` call according to Solidity's C3 linearisation of the root
|
|
39
|
+
* contract. `current` is the contract whose code contains the `super` call.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveSuper(root: ContractDefinition, current: ContractDefinition, signature: string): FunctionDefinition | undefined;
|
package/dist/utils.js
CHANGED
|
@@ -58,6 +58,12 @@ exports.getDeepRef = getDeepRef;
|
|
|
58
58
|
exports.getDefinitions = getDefinitions;
|
|
59
59
|
exports.toSource = toSource;
|
|
60
60
|
exports.getCallType = getCallType;
|
|
61
|
+
exports.getSignature = getSignature;
|
|
62
|
+
exports.getEnclosingContract = getEnclosingContract;
|
|
63
|
+
exports.resolveOverride = resolveOverride;
|
|
64
|
+
exports.idToContract = idToContract;
|
|
65
|
+
exports.forwardLinearization = forwardLinearization;
|
|
66
|
+
exports.resolveSuper = resolveSuper;
|
|
61
67
|
const fs = __importStar(require("fs/promises"));
|
|
62
68
|
const path = __importStar(require("path"));
|
|
63
69
|
const solc_typed_ast_1 = require("solc-typed-ast");
|
|
@@ -349,3 +355,84 @@ const signatureEquals = (a, b) => {
|
|
|
349
355
|
return a.stateMutability === b.stateMutability;
|
|
350
356
|
};
|
|
351
357
|
exports.signatureEquals = signatureEquals;
|
|
358
|
+
function getSignature(definition) {
|
|
359
|
+
let name;
|
|
360
|
+
if (definition instanceof solc_typed_ast_1.FunctionDefinition) {
|
|
361
|
+
if (definition.kind === solc_typed_ast_1.FunctionKind.Constructor) {
|
|
362
|
+
name = 'constructor';
|
|
363
|
+
}
|
|
364
|
+
else if (definition.kind === solc_typed_ast_1.FunctionKind.Fallback) {
|
|
365
|
+
name = 'fallback';
|
|
366
|
+
}
|
|
367
|
+
else if (definition.kind === solc_typed_ast_1.FunctionKind.Receive) {
|
|
368
|
+
name = 'receive';
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
name = definition.name;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
name = definition.name;
|
|
376
|
+
}
|
|
377
|
+
const paramTypes = definition.vParameters.vParameters.map(param => param.typeString).join(',');
|
|
378
|
+
return `${name}(${paramTypes})`;
|
|
379
|
+
}
|
|
380
|
+
function getEnclosingContract(node) {
|
|
381
|
+
let cur = node;
|
|
382
|
+
while (cur) {
|
|
383
|
+
if (cur instanceof solc_typed_ast_1.FunctionDefinition && cur.vScope instanceof solc_typed_ast_1.ContractDefinition) {
|
|
384
|
+
return cur.vScope;
|
|
385
|
+
}
|
|
386
|
+
cur = cur.parent;
|
|
387
|
+
}
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
function resolveOverride(root, signature) {
|
|
391
|
+
for (const ctr of forwardLinearization(root)) {
|
|
392
|
+
const match = ctr.vFunctions.find(fn => getSignature(fn) === signature);
|
|
393
|
+
if (match)
|
|
394
|
+
return match;
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
function idToContract(root, id) {
|
|
399
|
+
try {
|
|
400
|
+
return root.requiredContext.locate(id);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Build forward (source-order) linearized list: root, then direct bases left->right, then their bases recursively without duplicates.
|
|
407
|
+
function forwardLinearization(root) {
|
|
408
|
+
// Solidity compiler provides the C3 linearized order in `linearizedBaseContracts`.
|
|
409
|
+
// We rely on that order directly; it already starts with `root` and follows the
|
|
410
|
+
// resolution order used for `super`.
|
|
411
|
+
const ids = root.linearizedBaseContracts;
|
|
412
|
+
const contracts = [];
|
|
413
|
+
for (const id of ids) {
|
|
414
|
+
const ctr = idToContract(root, id);
|
|
415
|
+
if (ctr)
|
|
416
|
+
contracts.push(ctr);
|
|
417
|
+
}
|
|
418
|
+
return contracts;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Resolve the `super` call according to Solidity's C3 linearisation of the root
|
|
422
|
+
* contract. `current` is the contract whose code contains the `super` call.
|
|
423
|
+
*/
|
|
424
|
+
function resolveSuper(root, current, signature) {
|
|
425
|
+
// Linearised list of contract IDs for `root` (e.g. [D, B, C, A])
|
|
426
|
+
const linearContracts = forwardLinearization(root);
|
|
427
|
+
// Find index of the current contract
|
|
428
|
+
const idx = linearContracts.findIndex(c => c.id === current.id);
|
|
429
|
+
if (idx === -1)
|
|
430
|
+
return undefined;
|
|
431
|
+
// Search forward for first contract implementing the function
|
|
432
|
+
for (let i = idx + 1; i < linearContracts.length; i++) {
|
|
433
|
+
const cand = linearContracts[i].vFunctions.find(fn => getSignature(fn) === signature);
|
|
434
|
+
if (cand)
|
|
435
|
+
return cand;
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|