Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callgraph: Add effects #227

Merged
merged 5 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/detectors/builtin/sendInLoop.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CompilationUnit } from "../../internals/ir";
import { CallGraph } from "../../internals/ir/callGraph";
import { CallGraph, EffectFlags } from "../../internals/ir/callGraph";
import { forEachStatement, foldExpressions } from "../../internals/tact";
import { isSendCall } from "../../internals/tact/util";
import { MistiTactWarning, Severity } from "../../internals/warnings";
Expand Down Expand Up @@ -120,7 +120,10 @@ export class SendInLoop extends ASTDetector {
const calleeNodeId = callGraph.getNodeIdByName(calleeName);
if (calleeNodeId !== undefined) {
const calleeNode = callGraph.getNode(calleeNodeId);
if (calleeNode && calleeNode.hasFlag(0b0001)) {
if (
calleeNode &&
calleeNode.hasEffect(EffectFlags.CALLS_SEND)
) {
const functionName = calleeNode.name.includes("::")
? calleeNode.name.split("::").pop()
: calleeNode.name;
Expand Down
186 changes: 152 additions & 34 deletions src/internals/ir/callGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { TactASTStore } from "./astStore";
import { IdxGenerator } from "./indices";
import { MistiContext } from "../../";
import { Logger } from "../../internals/logger";
import { findInExpressions, forEachExpression } from "../tact/iterators";
import { forEachExpression } from "../tact/iterators";
import { isSendCall } from "../tact/util";
import {
AstFunctionDef,
Expand All @@ -16,22 +16,31 @@ import {
AstId,
AstContractDeclaration,
AstNode,
AstFieldAccess,
AstStatement,
AstStatementAssign,
AstStatementAugmentedAssign,
AstStatementExpression,
} from "@tact-lang/compiler/dist/grammar/ast";

export type CGNodeId = number & { readonly brand: unique symbol };
export type CGEdgeId = number & { readonly brand: unique symbol };

/**
* Flag constants for CGNode.
* Effect flags for CGNode.
*
* `FLAG_CALLS_SEND` (0b0001): Indicates that the function represented by this node
* contains a direct or indirect call to a "send" function.
* Each flag represents an effect or property of the function represented by the node.
*/
const FLAG_CALLS_SEND = 0b0001;
export enum EffectFlags {
CALLS_SEND = 1 << 0,
CONTRACT_STATE_READ = 1 << 1,
CONTRACT_STATE_WRITE = 1 << 2,
ACCESSES_DATETIME = 1 << 3,
RANDOMNESS = 1 << 4,
Esorat marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Represents an edge in the call graph, indicating a call from one function to another.
* Each edge has a unique index (`idx`) generated using `IdxGenerator`.
*/
class CGEdge {
public idx: CGEdgeId;
Expand All @@ -50,13 +59,12 @@ class CGEdge {

/**
* Represents a node in the call graph, corresponding to a function or method.
* Nodes maintain references to incoming and outgoing edges.
*/
class CGNode {
public idx: CGNodeId;
public inEdges: Set<CGEdgeId> = new Set();
public outEdges: Set<CGEdgeId> = new Set();
public flags: number = 0;
public effects: number = 0;

/**
* @param astId The AST ID of the function or method this node represents (can be `undefined` for synthetic nodes)
Expand All @@ -74,23 +82,18 @@ class CGNode {
}
}

// Method to set a flag
public setFlag(flag: number) {
this.flags |= flag;
public addEffect(effect: EffectFlags) {
this.effects |= effect;
}

// Method to check if a flag is set
public hasFlag(flag: number): boolean {
return (this.flags & flag) !== 0;
public hasEffect(effect: EffectFlags): boolean {
return (this.effects & effect) !== 0;
}
}

/**
* Represents the call graph, a directed graph where nodes represent functions or methods,
* and edges indicate calls between them.
*
* The `CallGraph` class provides methods to build the graph from a Tact AST, retrieve nodes/edges,
* analyze connectivity, and add function calls dynamically.
*/
export class CallGraph {
private nodeMap: Map<CGNodeId, CGNode> = new Map();
Expand Down Expand Up @@ -298,45 +301,73 @@ export class CallGraph {
| AstReceiver;
const funcNodeId = this.astIdToNodeId.get(func.id);
if (funcNodeId !== undefined) {
const funcNode = this.getNode(funcNodeId);
if (!funcNode) continue;

if ("statements" in func && func.statements) {
for (const stmt of func.statements) {
this.processStatement(stmt, funcNodeId, contractName);
}
}

forEachExpression(func, (expr) => {
this.processExpression(expr, funcNodeId, contractName);
});
const sendCallFound =
findInExpressions(func, isSendCall) !== null;
if (sendCallFound) {
const funcNode = this.getNode(funcNodeId);
if (funcNode) {
funcNode.setFlag(FLAG_CALLS_SEND);
}
}
}
}
}
} else if (entry.kind === "function_def") {
const func = entry as AstFunctionDef;
const funcNodeId = this.astIdToNodeId.get(func.id);
if (funcNodeId !== undefined) {
const funcNode = this.getNode(funcNodeId);
if (!funcNode) continue;
if (func.statements) {
for (const stmt of func.statements) {
this.processStatement(stmt, funcNodeId);
}
}
forEachExpression(func, (expr) => {
this.processExpression(expr, funcNodeId);
});
const sendCallFound = findInExpressions(func, isSendCall) !== null;
if (sendCallFound) {
const funcNode = this.getNode(funcNodeId);
if (funcNode) {
funcNode.setFlag(FLAG_CALLS_SEND);
}
}
}
}
}
}

/**
* Processes a single expression, identifying function or method calls to create edges.
* @param expr The expression to process.
* Processes a single statement, identifying assignments and other statements.
* Also detects effects and sets corresponding flags on the function node.
* @param stmt The statement to process.
* @param callerId The node ID of the calling function.
* @param currentContractName The name of the contract, if applicable.
*/
private processStatement(
stmt: AstStatement,
callerId: CGNodeId,
currentContractName?: string,
) {
const funcNode = this.getNode(callerId);
if (!funcNode) {
return;
}
if (
stmt.kind === "statement_assign" ||
stmt.kind === "statement_augmentedassign"
) {
if (isContractStateWrite(stmt)) {
funcNode.addEffect(EffectFlags.CONTRACT_STATE_WRITE);
}
} else if (stmt.kind === "statement_expression") {
const stmtExpr = stmt as AstStatementExpression;
this.processExpression(
stmtExpr.expression,
callerId,
currentContractName,
);
}
}

private processExpression(
expr: AstExpression,
callerId: CGNodeId,
Expand All @@ -356,6 +387,23 @@ export class CallGraph {
);
}
}

const funcNode = this.getNode(callerId);
if (!funcNode) {
return;
}
if (isContractStateRead(expr)) {
funcNode.addEffect(EffectFlags.CONTRACT_STATE_READ);
}
if (accessesDatetime(expr)) {
funcNode.addEffect(EffectFlags.ACCESSES_DATETIME);
}
if (isRandomnessCall(expr)) {
funcNode.addEffect(EffectFlags.RANDOMNESS);
}
if (isSendCall(expr)) {
funcNode.addEffect(EffectFlags.CALLS_SEND);
}
}

/**
Expand Down Expand Up @@ -431,3 +479,73 @@ export class CallGraph {
export function isSelf(expr: AstExpression): boolean {
return expr.kind === "id" && (expr as AstId).text === "self";
}

/**
* Helper function to determine if an expression is a contract state read.
* @param expr The expression to check.
* @returns True if the expression reads from a state variable; otherwise, false.
*/
function isContractStateRead(expr: AstExpression): boolean {
if (expr.kind === "field_access") {
const fieldAccess = expr as AstFieldAccess;
if (fieldAccess.aggregate.kind === "id") {
const idExpr = fieldAccess.aggregate as AstId;
if (idExpr.text === "self") {
return true;
}
}
}
return false;
}

/**
* Helper function to determine if a statement is a contract state write.
* @param stmt The statement to check.
* @returns True if the statement writes to a state variable; otherwise, false.
*/
function isContractStateWrite(
Esorat marked this conversation as resolved.
Show resolved Hide resolved
stmt: AstStatementAssign | AstStatementAugmentedAssign,
): boolean {
const pathExpr = stmt.path;
if (pathExpr.kind === "field_access") {
const fieldAccess = pathExpr as AstFieldAccess;
if (fieldAccess.aggregate.kind === "id") {
const idExpr = fieldAccess.aggregate as AstId;
if (idExpr.text === "self") {
return true;
}
}
}
// Note: This function does not currently detect state writes via method calls on state variables (e.g., Map.set()).
// Handling such cases may require more advanced analysis involving the symbol table or data flow analysis.
return false;
}

/**
* Helper function to determine if an expression is a blockchain state read.
* @param expr The expression to check.
* @returns True if the statement writes to a state variable; otherwise, false.
*/
function accessesDatetime(expr: AstExpression): boolean {
if (expr.kind === "static_call") {
const staticCall = expr as AstStaticCall;
const functionName = staticCall.function?.text;
return functionName === "now" || functionName === "timestamp";
}
return false;
}

/**
* Helper function to determine if an expression is a randomness call.
* @param expr The expression to check.
* @returns True if the expression introduces randomness; otherwise, false.
*/
function isRandomnessCall(expr: AstExpression): boolean {
if (expr.kind === "static_call") {
const staticCall = expr as AstStaticCall;
const functionName = staticCall.function?.text;
const prgUseNames = new Set(["nativeRandom", "nativeRandomInterval"]);
return prgUseNames.has(functionName || "");
}
return false;
}
64 changes: 40 additions & 24 deletions test/all/sample-jetton.expected.callgraph.dot
Original file line number Diff line number Diff line change
Expand Up @@ -11,59 +11,75 @@ digraph "CallGraph" {
node_9 [label="JettonDefaultWallet::receiver_2832"];
node_10 [label="JettonDefaultWallet::receiver_2876"];
node_11 [label="JettonDefaultWallet::get_wallet_data"];
node_12 [label="context"];
node_13 [label="require"];
node_14 [label="SampleJetton::mint"];
node_15 [label="ctx::readForwardFee"];
node_16 [label="min"];
node_17 [label="ton"];
node_18 [label="contractAddress"];
node_19 [label="send"];
node_12 [label="require"];
node_13 [label="SampleJetton::mint"];
node_14 [label="context"];
node_15 [label="send"];
node_16 [label="ctx::readForwardFee"];
node_17 [label="min"];
node_18 [label="ton"];
node_19 [label="contractAddress"];
node_20 [label="JettonDefaultWallet::toCell"];
node_21 [label="myBalance"];
node_22 [label="msg::loadUint"];
node_23 [label="msg::loadCoins"];
node_2 -> node_12;
node_2 -> node_13;
node_2 -> node_12;
node_2 -> node_13;
node_2 -> node_14;
node_2 -> node_12;
node_2 -> node_12;
node_2 -> node_13;
node_3 -> node_12;
node_3 -> node_13;
node_3 -> node_14;
node_3 -> node_12;
node_3 -> node_13;
node_4 -> node_12;
node_4 -> node_14;
node_4 -> node_12;
node_4 -> node_13;
node_6 -> node_12;
node_6 -> node_13;
node_6 -> node_15;
node_6 -> node_12;
node_6 -> node_12;
node_6 -> node_15;
node_6 -> node_13;
node_6 -> node_14;
node_6 -> node_12;
node_6 -> node_16;
node_6 -> node_16;
node_6 -> node_12;
node_6 -> node_17;
node_6 -> node_13;
node_6 -> node_18;
node_6 -> node_12;
node_6 -> node_19;
node_6 -> node_15;
node_6 -> node_20;
node_7 -> node_12;
node_7 -> node_13;
node_7 -> node_18;
node_7 -> node_13;
node_7 -> node_14;
node_7 -> node_12;
node_7 -> node_19;
node_7 -> node_12;
node_7 -> node_15;
node_7 -> node_20;
node_7 -> node_8;
node_7 -> node_16;
node_7 -> node_15;
node_7 -> node_19;
node_7 -> node_20;
node_8 -> node_21;
node_8 -> node_16;
node_8 -> node_17;
node_9 -> node_12;
node_9 -> node_12;
node_9 -> node_12;
node_9 -> node_15;
node_9 -> node_14;
node_9 -> node_12;
node_9 -> node_12;
node_9 -> node_16;
node_9 -> node_12;
node_9 -> node_13;
node_9 -> node_13;
node_9 -> node_15;
node_9 -> node_13;
node_9 -> node_19;
node_9 -> node_20;
node_10 -> node_12;
node_10 -> node_22;
node_10 -> node_22;
node_10 -> node_23;
node_10 -> node_13;
node_10 -> node_12;
}
Loading