Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
31 changes: 11 additions & 20 deletions PR_DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
## Summary

This pull request introduces a new security rule to detect potentially unsafe operations in Soroban contracts. The rule flags a variety of patterns that can lead to security vulnerabilities and unexpected behavior, providing developers with actionable recommendations to improve the safety and reliability of their contracts.
This pull request introduces a new optimization rule to detect and flag unnecessary storage writes in a contract's constructor. Redundant assignments to state variables during construction lead to wasted gas on deployment. This rule helps developers identify and eliminate these inefficiencies.

## What Changed

- Implemented a new security rule, `detect-unsafe-operations`, to identify unsafe patterns in Soroban contracts.
- The rule detects the following unsafe operations:
- `unsafe` blocks
- `unsafe fn`
- `.unwrap()` calls
- `panic!` macros
- `unreachable!` macros
- `std::mem::transmute`
- Raw pointer usage
- Unchecked arithmetic operators
- For each detected violation, the rule provides a detailed description of the issue and a recommendation for how to fix it.
- **Added Rule:** Implemented `detect-unnecessary-constructor-storage-writes.ts` to identify when a state variable is assigned a value more than once within the constructor.
- **Added Tests:** Created corresponding unit tests with mock contracts (`UnnecessaryWritesContract.sol` and `NoUnnecessaryWritesContract.sol`) to validate the rule's accuracy and prevent false positives.

## Why

The use of unsafe operations in Soroban contracts can introduce serious security vulnerabilities, such as memory corruption, integer overflows, and unexpected panics. By detecting these patterns and providing clear guidance on how to avoid them, this new rule helps developers write more secure and robust contracts.
Unnecessary storage writes during deployment are an anti-pattern that increases gas costs without providing any functional benefit. This rule helps developers write more efficient and cost-effective contracts by flagging these redundant assignments.

## Testing Performed

- [x] Lint
- [x] Tests
- [x] Build
- [x] Wrote unit tests for the new rule.
- [x] Manually verified the rule against several contracts.

## Edge Cases Considered

- The rule correctly handles a variety of code formatting and style variations.
- The rule avoids false positives by ignoring non-arithmetic uses of `+`, `-`, and `*` operators.
- Contracts with no constructor.
- Constructors with no storage writes.
- Constructors with multiple, non-redundant storage writes to different variables.

## Risks

None. The new rule is purely additive and does not introduce any breaking changes.
None. This is a non-breaking, additive change that only introduces a new check.

Closes #496
Closes #356
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@

import { detectUnnecessaryConstructorStorageWrites } from '../detect-unnecessary-constructor-storage-writes';

describe('detectUnnecessaryConstructorStorageWrites', () => {
it('should not detect any issues in a contract with no constructor', () => {
const code = `
contract NoConstructor {
uint256 public value;
}
`;
const result = detectUnnecessaryConstructorStorageWrites(code);
expect(result.detected).toBe(false);
});

it('should not detect any issues in a constructor with no assignments', () => {
const code = `
contract NoAssignments {
constructor() {}
}
`;
const result = detectUnnecessaryConstructorStorageWrites(code);
expect(result.detected).toBe(false);
});

it('should not detect any issues with single assignments', () => {
const code = `
contract SingleAssignments {
uint256 public owner;
uint256 public value;

constructor(uint256 _value) {
owner = msg.sender;
value = _value;
}
}
`;
const result = detectUnnecessaryConstructorStorageWrites(code);
expect(result.detected).toBe(false);
});

it('should detect redundant assignments in the constructor', () => {
const code = `
contract RedundantAssignments {
uint256 public value;

constructor(uint256 _initialValue) {
value = 100;
value = _initialValue;
}
}
`;
const result = detectUnnecessaryConstructorStorageWrites(code);
expect(result.detected).toBe(true);
expect(result.superfluousWrites.length).toBe(1);
expect(result.superfluousWrites[0].variableName).toBe('value');
expect(result.superfluousWrites[0].line).toBe(7);
});

it('should detect multiple redundant assignments for multiple variables', () => {
const code = `
contract MultipleRedundantAssignments {
uint256 public value;
address public owner;

constructor(uint256 _initialValue, address _newOwner) {
value = 100;
owner = address(0);
value = _initialValue;
owner = _newOwner;
}
}
`;
const result = detectUnnecessaryConstructorStorageWrites(code);
expect(result.detected).toBe(true);
expect(result.superfluousWrites.length).toBe(2);
expect(result.superfluousWrites[0].variableName).toBe('value');
expect(result.superfluousWrites[0].line).toBe(9);
expect(result.superfluousWrites[1].variableName).toBe('owner');
expect(result.superfluousWrites[1].line).toBe(10);
});

it('should handle complex constructor logic', () => {
const code = `
contract ComplexConstructor {
uint256 public value;

constructor(bool _condition, uint256 _value1, uint256 _value2) {
value = _value1;
if (_condition) {
value = _value2;
}
}
}
`;
const result = detectUnnecessaryConstructorStorageWrites(code);
expect(result.detected).toBe(true);
expect(result.superfluousWrites.length).toBe(1);
expect(result.superfluousWrites[0].variableName).toBe('value');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@

/**
* @title Detect Unnecessary Constructor Storage Writes
* @notice Identifies state variables that are assigned values multiple times within the constructor.
* @dev Redundant assignments increase deployment gas costs. This rule helps optimize gas usage by flagging unnecessary storage writes.
*/

export interface UnnecessaryWrite {
variableName: string;
line: number;
reason: string;
}

export interface UnnecessaryConstructorWriteResult {
detected: boolean;
superfluousWrites: UnnecessaryWrite[];
message: string;
suggestion: string;
}

function stripComments(code: string): string {
return code.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
}

function findConstructor(code: string): { startLine: number; endLine: number; body: string } | null {
const lines = code.split('\n');
const constructorPattern = /^\s*(constructor)\s*\([^)]*\)\s*(?:public\s*)?\{/;

for (let i = 0; i < lines.length; i++) {
if (constructorPattern.test(lines[i])) {
let braceDepth = 0;
const bodyLines: string[] = [];
let bodyStarted = false;

for (let j = i; j < lines.length; j++) {
const line = lines[j];
const opens = (line.match(/\{/g) || []).length;
const closes = (line.match(/\}/g) || []).length;

if (opens > 0) bodyStarted = true;
if (bodyStarted) bodyLines.push(line);
braceDepth += opens - closes;

if (bodyStarted && braceDepth === 0) {
return {
startLine: i + 1,
endLine: j + 1,
body: bodyLines.slice(1, -1).join('\n'),
};
}
}
}
}

return null;
}

export function detectUnnecessaryConstructorStorageWrites(code: string): UnnecessaryConstructorWriteResult {
const strippedCode = stripComments(code);
const constructor = findConstructor(strippedCode);

if (!constructor) {
return {
detected: false,
superfluousWrites: [],
message: 'No constructor found.',
suggestion: '',
};
}

const assignments = new Map<string, number[]>();
const lines = constructor.body.split('\n');

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const assignmentMatch = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=/);
if (assignmentMatch) {
const varName = assignmentMatch[1];
if (!assignments.has(varName)) {
assignments.set(varName, []);
}
assignments.get(varName)!.push(constructor.startLine + i);
}
}

const superfluousWrites: UnnecessaryWrite[] = [];
for (const [variableName, lineNumbers] of assignments.entries()) {
if (lineNumbers.length > 1) {
for (let i = 1; i < lineNumbers.length; i++) {
superfluousWrites.push({
variableName,
line: lineNumbers[i],
reason: `Redundant assignment to '${variableName}'. It was already assigned on line ${lineNumbers[0]}.`,
});
}
}
}

if (superfluousWrites.length > 0) {
return {
detected: true,
superfluousWrites,
message: 'Unnecessary storage writes detected in the constructor.',
suggestion: 'Remove redundant assignments to save deployment gas.',
};
}

return {
detected: false,
superfluousWrites: [],
message: 'No unnecessary constructor storage writes detected.',
suggestion: '',
};
}
Loading