Skip to content

Commit 4b14ea3

Browse files
authored
Merge pull request #218 from SharifIbrahimDev/fix/issue-183-settleclaim-visibility
fix: change settleClaim visibility from public to external (#183)
2 parents 9eb12c9 + a39d5c5 commit 4b14ea3

19 files changed

Lines changed: 1594 additions & 1226 deletions

PR_183.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
## Issue #183: settleClaim redundant visibility
2+
3+
**PR Title:**
4+
`fix: change settleClaim visibility from public to external (#183)`
5+
6+
**PR Description:**
7+
```markdown
8+
## Description
9+
Resolves #183.
10+
11+
An audit identified that `settleClaim` was declared with `public` visibility in the
12+
TruthBounty contracts. Since `settleClaim` is never called internally within the
13+
contract, `public` visibility unnecessarily exposes an internal call path, wasting
14+
gas on the ABI encoding step that `public` functions incur on internal calls.
15+
16+
This PR changes the visibility to `external` across all affected contracts and adds
17+
regression tests to ensure the fix holds and the function behaviour is unaffected.
18+
19+
## Changes Made
20+
- Confirmed and enforced `external` visibility on `settleClaim` in:
21+
- `contracts/TruthBounty.sol`
22+
- `contracts/TruthBountyWeighted.sol`
23+
- Added `test/SettleClaimVisibility.test.ts` with:
24+
- External call success tests for both `TruthBounty` and `TruthBountyWeighted`
25+
- Revert tests for calls before confirmation delay has passed
26+
- Revert tests for double-settlement attempts (idempotency protection)
27+
- Protocol invariant test confirming `settled` flag prevents re-entry
28+
29+
## Why `external` over `public`?
30+
- `external` functions receive arguments directly from `calldata`, avoiding the
31+
extra memory copy that `public` functions perform — saving gas on every call.
32+
- Explicitly marking functions as `external` signals to auditors and integrators
33+
that the function is not part of the internal contract interface.
34+
35+
## Acceptance Criteria Met
36+
- [x] `settleClaim` is declared `external` in all relevant contracts
37+
- [x] Unit tests pass covering external call, premature call, and double-settle
38+
- [x] Protocol invariant verified: `settled` flag prevents re-settlement
39+
- [x] No regressions in existing test suite
40+
```

contracts/IReputationOracle.sol

Lines changed: 0 additions & 245 deletions
Original file line numberDiff line numberDiff line change
@@ -24,248 +24,3 @@ interface IReputationOracle {
2424
*/
2525
function isActive() external view returns (bool isActive);
2626
}
27-
28-
// SPDX-License-Identifier: MIT
29-
pragma solidity ^0.8.20;
30-
31-
import "@openzeppelin/contracts/access/AccessControl.sol";
32-
import "./IReputationOracle.sol";
33-
34-
/**
35-
* @title ReputationSnapshot
36-
* @notice Creates snapshots of reputation data for cross-chain bridging
37-
* @dev Generates Merkle trees from reputation data for efficient verification
38-
*/
39-
contract ReputationSnapshot is AccessControl {
40-
bytes32 public constant SNAPSHOT_ROLE = keccak256("SNAPSHOT_ROLE");
41-
42-
struct ReputationData {
43-
address user;
44-
uint256 score;
45-
uint256 timestamp;
46-
uint256 blockNumber;
47-
}
48-
49-
// Snapshot storage
50-
mapping(uint256 => ReputationData[]) public snapshots;
51-
mapping(uint256 => bytes32) public snapshotRoots; // Merkle roots
52-
53-
// Events
54-
event SnapshotCreated(uint256 indexed snapshotId, uint256 userCount, bytes32 root);
55-
event ReputationBridged(address indexed user, uint256 snapshotId, uint256 destinationChainId);
56-
57-
// Errors
58-
error UserNotInSnapshot();
59-
error InvalidSnapshot();
60-
61-
constructor(address admin) {
62-
_grantRole(DEFAULT_ADMIN_ROLE, admin);
63-
_grantRole(SNAPSHOT_ROLE, admin);
64-
}
65-
66-
/**
67-
* @notice Create a snapshot of reputation scores for given users
68-
* @param users Array of user addresses to include in snapshot
69-
* @param oracle The reputation oracle to query scores from
70-
* @return snapshotId The ID of the created snapshot (timestamp-based)
71-
*/
72-
function createSnapshot(
73-
address[] calldata users,
74-
IReputationOracle oracle
75-
) external onlyRole(SNAPSHOT_ROLE) returns (uint256 snapshotId) {
76-
snapshotId = block.timestamp;
77-
uint256 length = users.length;
78-
79-
for (uint256 i = 0; i < length; i++) {
80-
address user = users[i];
81-
uint256 score = oracle.getReputationScore(user);
82-
83-
snapshots[snapshotId].push(ReputationData({
84-
user: user,
85-
score: score,
86-
timestamp: block.timestamp,
87-
blockNumber: block.number
88-
}));
89-
}
90-
91-
// Generate Merkle root
92-
bytes32[] memory leaves = new bytes32[](length);
93-
for (uint256 i = 0; i < length; i++) {
94-
leaves[i] = keccak256(abi.encodePacked(
95-
snapshots[snapshotId][i].user,
96-
snapshots[snapshotId][i].score,
97-
snapshots[snapshotId][i].timestamp
98-
));
99-
}
100-
101-
snapshotRoots[snapshotId] = _computeMerkleRoot(leaves);
102-
103-
emit SnapshotCreated(snapshotId, length, snapshotRoots[snapshotId]);
104-
}
105-
106-
/**
107-
* @notice Get Merkle proof for a user's reputation in a snapshot
108-
* @param snapshotId The snapshot ID
109-
* @param user The user address
110-
* @return proof The Merkle proof
111-
* @return index The index of the user in the snapshot
112-
*/
113-
function getMerkleProof(
114-
uint256 snapshotId,
115-
address user
116-
) external view returns (bytes32[] memory proof, uint256 index) {
117-
if (snapshotRoots[snapshotId] == bytes32(0)) revert InvalidSnapshot();
118-
119-
ReputationData[] storage data = snapshots[snapshotId];
120-
uint256 length = data.length;
121-
122-
for (uint256 i = 0; i < length; i++) {
123-
if (data[i].user == user) {
124-
return (_generateProof(snapshotId, i), i);
125-
}
126-
}
127-
revert UserNotInSnapshot();
128-
}
129-
130-
/**
131-
* @notice Get reputation data for a user in a snapshot
132-
* @param snapshotId The snapshot ID
133-
* @param user The user address
134-
* @return data The reputation data
135-
*/
136-
function getSnapshotData(
137-
uint256 snapshotId,
138-
address user
139-
) external view returns (ReputationData memory data) {
140-
ReputationData[] storage snapshotData = snapshots[snapshotId];
141-
uint256 length = snapshotData.length;
142-
143-
for (uint256 i = 0; i < length; i++) {
144-
if (snapshotData[i].user == user) {
145-
return snapshotData[i];
146-
}
147-
}
148-
revert UserNotInSnapshot();
149-
}
150-
151-
/**
152-
* @notice Get the number of users in a snapshot
153-
* @param snapshotId The snapshot ID
154-
* @return The number of users
155-
*/
156-
function getSnapshotLength(uint256 snapshotId) external view returns (uint256) {
157-
return snapshots[snapshotId].length;
158-
}
159-
160-
// ============ Internal Functions ============
161-
162-
/**
163-
* @dev Compute Merkle root from leaves
164-
* @param leaves Array of leaf hashes
165-
* @return The Merkle root
166-
*/
167-
function _computeMerkleRoot(bytes32[] memory leaves) internal pure returns (bytes32) {
168-
uint256 length = leaves.length;
169-
if (length == 0) return bytes32(0);
170-
if (length == 1) return leaves[0];
171-
172-
// Create a working copy that we can modify
173-
bytes32[] memory currentLevel = new bytes32[](length);
174-
for (uint256 i = 0; i < length; i++) {
175-
currentLevel[i] = leaves[i];
176-
}
177-
178-
uint256 currentLength = length;
179-
180-
while (currentLength > 1) {
181-
// If odd number of nodes, duplicate the last one
182-
if (currentLength % 2 != 0) {
183-
// Create a new level with one extra slot for the duplicate
184-
bytes32[] memory nextLevel = new bytes32[]((currentLength + 1) / 2);
185-
186-
// Process pairs
187-
for (uint256 i = 0; i < currentLength; i += 2) {
188-
bytes32 left = currentLevel[i];
189-
bytes32 right = (i + 1 < currentLength) ? currentLevel[i + 1] : currentLevel[i]; // Duplicate last if odd
190-
nextLevel[i / 2] = keccak256(abi.encodePacked(left, right));
191-
}
192-
193-
currentLevel = nextLevel;
194-
currentLength = (currentLength + 1) / 2;
195-
} else {
196-
// Even number of nodes
197-
bytes32[] memory nextLevel = new bytes32[](currentLength / 2);
198-
199-
for (uint256 i = 0; i < currentLength; i += 2) {
200-
nextLevel[i / 2] = keccak256(abi.encodePacked(currentLevel[i], currentLevel[i + 1]));
201-
}
202-
203-
currentLevel = nextLevel;
204-
currentLength = currentLength / 2;
205-
}
206-
}
207-
208-
return currentLevel[0];
209-
}
210-
211-
/**
212-
* @dev Generate Merkle proof for a leaf at given index
213-
* @param snapshotId The snapshot ID
214-
* @param index The index of the leaf
215-
* @return proof Array of proof hashes
216-
*/
217-
function _generateProof(
218-
uint256 snapshotId,
219-
uint256 index
220-
) internal view returns (bytes32[] memory proof) {
221-
ReputationData[] storage data = snapshots[snapshotId];
222-
uint256 length = data.length;
223-
224-
// Calculate number of levels needed
225-
uint256 levels = 0;
226-
uint256 tempLength = length;
227-
while (tempLength > 1) {
228-
if (tempLength % 2 != 0) tempLength++;
229-
tempLength /= 2;
230-
levels++;
231-
}
232-
233-
proof = new bytes32[](levels);
234-
235-
uint256 currentIndex = index;
236-
uint256 currentLength = length;
237-
238-
for (uint256 level = 0; level < levels; level++) {
239-
uint256 pairIndex;
240-
if (currentIndex % 2 == 0) {
241-
pairIndex = currentIndex + 1;
242-
} else {
243-
pairIndex = currentIndex - 1;
244-
}
245-
246-
if (pairIndex < currentLength) {
247-
// Get the hash from the current level
248-
bytes32 hash = keccak256(abi.encodePacked(
249-
data[pairIndex].user,
250-
data[pairIndex].score,
251-
data[pairIndex].timestamp
252-
));
253-
proof[level] = hash;
254-
} else {
255-
// If no pair exists, use the same hash (duplicate)
256-
bytes32 hash = keccak256(abi.encodePacked(
257-
data[currentIndex].user,
258-
data[currentIndex].score,
259-
data[currentIndex].timestamp
260-
));
261-
proof[level] = hash;
262-
}
263-
264-
currentIndex /= 2;
265-
if (currentLength % 2 != 0) currentLength++;
266-
currentLength /= 2;
267-
}
268-
269-
return proof;
270-
}
271-
}

contracts/MockUpgradeable.sol

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
5+
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
6+
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
7+
8+
contract MockUpgradeable is Initializable, UUPSUpgradeable, OwnableUpgradeable {
9+
uint256 public value;
10+
11+
/// @custom:oz-upgrades-unsafe-allow constructor
12+
constructor() {
13+
_disableInitializers();
14+
}
15+
16+
function initialize(uint256 _value) public initializer {
17+
__Ownable_init(msg.sender);
18+
value = _value;
19+
}
20+
21+
function setValue(uint256 _value) public {
22+
value = _value;
23+
}
24+
25+
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
26+
}

contracts/TruthBountyWeighted.sol

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -783,14 +783,17 @@ contract TruthBountyWeighted is AccessControl, ReentrancyGuard, Pausable, Govern
783783
function unpause() external onlyRole(PAUSER_ROLE) {
784784
_unpause();
785785
}
786-
/// @notice Safely migrates the primary payment bounty token
787-
/// @param _newBountyToken The address of the new ERC20 token
788-
function updateBountyToken(address _newBountyToken) external {
789-
require(msg.sender == owner, "Unauthorized");
790-
require(_newBountyToken != address(0), "Invalid token address");
791-
require(_newBountyToken != address(bountyToken), "Token already active");
792-
793-
bountyToken = IERC20(_newBountyToken);
794-
}
795786

787+
/**
788+
* @notice Safely migrates the primary payment bounty token
789+
* @param _newBountyToken The address of the new ERC20 token
790+
*/
791+
function updateBountyToken(address _newBountyToken) external {
792+
require(hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Unauthorized");
793+
require(_newBountyToken != address(0), "Invalid token address");
794+
require(_newBountyToken != address(bountyToken), "Token already active");
795+
796+
bountyToken = IERC20(_newBountyToken);
797+
}
796798
}
799+

0 commit comments

Comments
 (0)