diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md index eedf9fb..6cc57f7 100644 --- a/PR_DESCRIPTION.md +++ b/PR_DESCRIPTION.md @@ -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 \ No newline at end of file +Closes #356 \ No newline at end of file diff --git a/rules/optimization/deployment/__tests__/detect-unnecessary-constructor-storage-writes.spec.ts b/rules/optimization/deployment/__tests__/detect-unnecessary-constructor-storage-writes.spec.ts new file mode 100644 index 0000000..59c6837 --- /dev/null +++ b/rules/optimization/deployment/__tests__/detect-unnecessary-constructor-storage-writes.spec.ts @@ -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'); + }); +}); \ No newline at end of file diff --git a/rules/optimization/deployment/detect-unnecessary-constructor-storage-writes.ts b/rules/optimization/deployment/detect-unnecessary-constructor-storage-writes.ts new file mode 100644 index 0000000..afd5182 --- /dev/null +++ b/rules/optimization/deployment/detect-unnecessary-constructor-storage-writes.ts @@ -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(); + 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: '', + }; +} \ No newline at end of file