MyCut

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

Unnecessary Gas Consumption Due To Inclusion Of ZeroReward Participants

Github Link

https://github.com/Cyfrin/2024-08-MyCut/blob/946231db0fe717039429a11706717be568d03b54/src/ContestManager.sol#L16C4-L22C66

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

Summary

The audit of the smart contract has identified a potential vulnerability in the ContestManager::createContest function, which arises from the absence of initial filtering for zero rewards. This oversight results in the inclusion of players with zero-value rewards in the reward distribution array, leading to unnecessary gas consumption during the reward distribution process. Specifically, the function iterates over an array of players and assigns rewards without considering whether the rewards are non-zero:

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

Furthermore, the claimCut function checks whether the reward is zero before transferring it to the player, which is also applicable when there are no players in the array itself. Therefore, excluding zeros during deployment directly will not affect the logic of the code, while also avoiding the waste of gas.

Impact

  • High: The unnecessary inclusion of zero rewards in the reward distribution process can lead to significant gas wastage, especially when dealing with a large number of participants.

  • High: The inefficient check for zero rewards in the claimCut function can also lead to unnecessary gas consumption, as it does not optimize for the scenario where a player has no reward.

Proof

Use Foundry Tools and Test File:

Tool Used: Foundry (forge 0.2.0)
Test File: TestMyCut.t.sol

A loop from i = 3 to 9999 that creates player names and addresses,with 0 reward,and then `uint gasUsedCreateAndFund = gasStart - gasAfterPot;` is how gas we CreatePot().

// 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(0));
}
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 testUnclaimedRewardDistribution -vvvvv

  • Terminal Output from Foundry:

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

The next test is to calculate the gas consumed during the deployment of the pot after excluding participants with zero rewards prior to deployment.

// 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(0));
// }
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 testUnclaimedRewardDistribution -vvvvv

  • Terminal Output from Foundry:

├─ [0] console::log("Gas used for create and fund: %d", 713439 [7.134e5]) [staticcall]

From the logs:

* The transaction including zero rewards used approximately 333.63 million gas units.
* The transaction excluding zero rewards used approximately 713.44 thousand gas units.

The excess gas, wasted due to the inclusion of zero rewards participants, is calculated to be about 332.91 million gas units. This highlights the importance of optimizing the contract deployment by filtering out participants with zero rewards to save on gas costs and enhance efficiency. By excluding these participants before deployment, the contract can operate more cost-effectively, making it more sustainable for large-scale applications.

Recommendations

Here's an example of how you might write a JavaScript script using Ethers.js to verify the data before deploying a smart contract that includes checks for zero rewards.

const { ethers } = require("ethers");
const abi = [/* ABI of your contract */];
const bytecode = "0x..."; // Bytecode of your contract
// Function to verify the players and rewards arrays
async function verifyContestData(players, rewards) {
// Check if players and rewards arrays are of the same length
if (players.length !== rewards.length) {
throw new Error("Players and rewards arrays must be of the same length.");
}
// Check for zero rewards
for (let i = 0; i < rewards.length; i++) {
if (rewards[i] === 0) {
throw new Error(`Invalid reward value at index ${i}: rewards must be greater than zero.`);
}
}
}
// Function to deploy the contract
async function deployContract(signer, players, rewards, tokenAddress, totalRewards) {
// First, verify the data
await verifyContestData(players, rewards);
// Create a contract factory
const factory = new ethers.ContractFactory(abi, bytecode, signer);
// Deploy the contract
const contestManager = await factory.deploy();
await contestManager.deployed();
console.log(`Contract deployed to: ${contestManager.address}`);
// Call the contract's createContest function
const potAddress = await contestManager.createContest(players, rewards, tokenAddress, totalRewards);
console.log(`Pot created at: ${potAddress}`);
}
// Example usage
const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545");
const wallet = new ethers.Wallet("YOUR_PRIVATE_KEY", provider);
const signer = wallet.connect(provider);
const players = ["0x...1", "0x...2", "0x...3"]; // Player addresses
const rewards = [100, 200, 300]; // Reward amounts
const tokenAddress = "0x..."; // Token contract address
const totalRewards = 600; // Total rewards amount
// Deploy the contract
deployContract(signer, players, rewards, tokenAddress, totalRewards)
.then(() => console.log("Deployment successful"))
.catch((error) => console.error("Deployment failed:", error));

Please make sure to replace the abi and bytecode variables with the actual ABI and bytecode of your contract. Additionally, YOUR_PRIVATE_KEY should be replaced with the private key of your Ethereum wallet.

This script first defines a verifyContestData function to validate the players and rewards arrays. Then, the deployContract function is used to deploy the contract and call the createContest function. Before deployment, it calls verifyContestData to ensure there are no zero rewards. If the verification fails, an error is thrown and the deployment process is halted.

Updates

Lead Judging Commences

equious Lead Judge 12 months ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

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