MyCut

First Flight #23
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: low
Invalid

Cache array length outside of loop

Github Link

https://github.com/Cyfrin/2024-08-MyCut/blob/946231db0fe717039429a11706717be568d03b54/src/Pot.sol#L32
https://github.com/Cyfrin/2024-08-MyCut/blob/946231db0fe717039429a11706717be568d03b54/src/Pot.sol#L57

Vulnerability Details

If not cached, the solidity compiler will always read the length of the array during each iteration.

Impact

That is, if it is a storage array, this is an extra sload operation (100 additional extra gas for each iteration except for the first) and if it is a memory array, this is an extra mload operation (3 additional gas for each iteration except for the first).

proof code

before catched:

for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}

after catched:

uint 256 Player_length = i_players.length
for (uint256 i = 0; i < Player_length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}

use Foundry to text

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ContestManager} from "../src/ContestManager.sol";
import {Test, console} from "lib/forge-std/src/Test.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {ERC20Mock} from "./ERC20Mock.sol";
import {Strings} from "lib/openzeppelin-contracts/contracts/utils/Strings.sol";
import {Pot} from "../src/Pot.sol";
contract TestMyCut is Test {
address conMan;
address player1 = makeAddr("player1");
address player2 = makeAddr("player2");
address[] players = [player1, player2];
uint256 public constant STARTING_USER_BALANCE = 1000;
ERC20Mock weth;
address contest;
address[] totalContests;
uint256[] rewards = [1, 10];
address user = makeAddr("user");
uint256 totalRewards = 11;
function setUp() public {
for (uint256 i = 3; i < 10000; i++) {
string memory playerName = string(abi.encodePacked("player", Strings.toString(i)));
address playerAddr = makeAddr(playerName);
players.push(playerAddr);
rewards.push(uint256(i));
}
vm.startPrank(user);
// DeployContestManager deploy = new DeployContestManager();
conMan = address(new ContestManager());
weth = new ERC20Mock("WETH", "WETH", msg.sender, 11);
uint256 managerBalanceafter = ERC20Mock(weth).balanceOf(conMan);
uint256 manageruserafter = ERC20Mock(weth).balanceOf(user);
vm.stopPrank();
}
modifier mintAndApproveTokens() {
console.log("Minting tokens to: ", user);
vm.startPrank(user);
ERC20Mock(weth).mint(user, STARTING_USER_BALANCE);
ERC20Mock(weth).approve(conMan, STARTING_USER_BALANCE);
console.log("Approved tokens to: ", address(conMan));
vm.stopPrank();
_;
}
function testCanCreatePot() public mintAndApproveTokens {
console.log("Contest Manager Owner: ", ContestManager(conMan).owner());
console.log("msg.sender: ", msg.sender);
vm.startPrank(user);
uint gasStart = gasleft();
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), totalRewards);
uint gasAfterPot = gasleft();
uint gasUsedCreateAndFund = gasStart - gasAfterPot;
console.log("Gas used for create and fund: %d", gasUsedCreateAndFund);
totalContests = ContestManager(conMan).getContests();
vm.stopPrank();
assertEq(totalContests.length, 1);
}
  • Run forge test --mt testCanCreatePot -vvvvv
    Before

├─ [0] console::log("Gas used for create and fund: %d", 731507944 [7.315e8]) [staticcall]

After:

├─ [0] console::log("Gas used for create and fund: %d", 730508049 [7.305e8]) [staticcall]

To calculate the specific amount of gas saved after the function optimization, we can subtract the lower gas usage from the higher one.This calculation shows that the optimization saved approximately 999,895 gas units.

English Translation:

After the function was optimized by caching the array length, a significant amount of gas was saved when deploying the contract. Assuming 10,000 participants were involved in the reward claim process, the specific amount of gas saved can be calculated by subtracting the gas usage after the optimization from the initial gas usage:

[ \text{Gas Saved} = 731,507,944 - 730,508,049 ]
[ \text{Gas Saved} = 999,895 ]

This indicates that the optimization resulted in a savings of approximately 999,895 gas units.

Recommendations

Here's the original code with the necessary change to cache the length of the array for optimization:

-for (uint256 i = 0; i <i_players.length; i++) {
- playersToRewards[i_players[i]] = i_rewards[i];
-}
// ++++ Cache the array length to avoid repeated reads.
+uint256 playersLength = i_players.length; // Cache the length
+for (uint256 i = 0; i < playersLength; i++) {
+ playersToRewards[i_players[i]] = i_rewards[i];
+}
// ----- No need to read the length in each iteration.

By caching the length of the i_players array, you ensure that it's only read once, rather than on each iteration of the loop. This small change can lead to a significant reduction in gas costs, especially when dealing with large arrays.

Updates

Lead Judging Commences

equious Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.