Project

One World
NFTDeFi
15,000 USDC
View results
Submission Details
Severity: medium
Invalid

Gas optimization to avoid DOS attacks

Summary: Gas optimization techniques themselves are not inherently exploitable by attackers. They are generally implemented to make smart contract operations more efficient and cost-effective. However, improper implementation or misunderstanding of gas optimization can inadvertently introduce vulnerabilities.

/// @notice Updates profit tracking after a claim
/// @param account The account updating profits for
/// @return profit The updated saved profit
function saveProfit(address account) internal returns (uint256 profit) {
uint256 unsaved = getUnsaved(account);
lastProfit[account] = totalProfit;
profit = savedProfit[account] + unsaved;
savedProfit[account] = profit;
}

Vulnerability Details:

  • Over-Optimization:

    • Risk: Over-optimizing for gas can sometimes obscure the clarity of the code, making it harder to maintain and understand. This might result in overlooking security issues.

    • Precaution: Balance optimization with code readability and maintainability. Regularly review and test the optimized code for security vulnerabilities.

  • Assumptions About Gas Costs:

    • Risk: Optimizations that assume certain gas costs might become less effective if the gas cost model changes (e.g., Ethereum gas cost adjustments).

    • Precaution: Stay informed about gas cost updates and test your contract under different scenarios to ensure optimizations remain valid.

Impact: Gas optimization helps make smart contract execution more efficient and cost-effective.

  1. Inadvertent Vulnerabilities:

    • Risk: Improper implementation of gas optimization techniques may introduce new vulnerabilities, such as incorrect caching or over-optimization, leading to logic errors.

    • Impact: These vulnerabilities could be exploited to manipulate contract behavior, potentially leading to financial losses or malfunctioning smart contracts.

  2. Denial of Service (DoS) Attacks:

    • Risk: Gas optimization might inadvertently lead to scenarios where certain functions become too expensive or complex, causing transactions to fail due to exceeding the gas limit.

    • Impact: Attackers can exploit this to create DoS conditions, preventing legitimate users from interacting with the contract.

  3. Security and Integrity Issues:

    • Risk: Optimization efforts that obscure the clarity of the code can make it harder to identify and fix security issues.

    • Impact: This can lead to bugs and vulnerabilities remaining undetected, compromising the contract's security and integrity.

  4. Increased Complexity:

    • Risk: Over-optimization can make the code overly complex, which may introduce subtle bugs that are hard to detect and fix.

    • Impact: Complex code is more prone to errors and harder to maintain, leading to potential long-term security and operational challenges.

Specific Examples:

  1. Incorrect Caching of Values:

    • Issue: Incorrectly caching the length of an array and assuming it remains unchanged can lead to incorrect results if the array is modified.

    • Impact: This can result in incorrect calculations and state updates, potentially allowing attackers to exploit these inconsistencies.

  2. State Write Minimization Errors:

    • Issue: Reducing repeated state writes without thorough checks may overlook necessary updates, leaving the contract state inconsistent.

    • Impact: Attackers could exploit these inconsistencies to manipulate the contract state, causing unauthorized gains or losses.

  3. Mismanagement of calldata:

    • Issue: Improper use of calldata for function parameters meant to be modified can lead to unexpected behavior and security loopholes.

    • Impact: This can result in incorrect data handling and potential exploits.

Proof of Concept Code: Vulnerable Contract Code

Here's a simplified version of your contract that includes potential vulnerabilities:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract VulnerableContract is ERC1155, AccessControl {
bytes32 public constant OWP_FACTORY_ROLE = keccak256("OWP_FACTORY_ROLE");
uint256 public totalProfit;
mapping(address => uint256) public lastProfit;
mapping(address => uint256) public savedProfit;
uint256 internal constant ACCURACY = 1e30;
constructor() ERC1155("") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(OWP_FACTORY_ROLE, msg.sender);
}
function saveProfit(address account) internal returns (uint256 profit) {
uint256 unsaved = getUnsaved(account);
lastProfit[account] = totalProfit;
profit = savedProfit[account] + unsaved;
savedProfit[account] = profit;
}
function getUnsaved(address account) internal view returns (uint256) {
return ((totalProfit - lastProfit[account]) * balanceOf(account, 0)) / ACCURACY;
}
function claimProfit() external returns (uint256 profit) {
profit = saveProfit(msg.sender);
require(profit > 0, "No profit available");
savedProfit[msg.sender] = 0;
IERC20(currency).safeTransfer(msg.sender, profit);
emit Claim(msg.sender, profit);
}
function sendProfit(uint256 amount) external {
uint256 _totalSupply = totalSupply;
if (_totalSupply > 0) {
totalProfit += (amount * ACCURACY) / _totalSupply;
IERC20(currency).safeTransferFrom(msg.sender, address(this), amount);
emit Profit(amount);
} else {
IERC20(currency).safeTransferFrom(msg.sender, creator, amount); // Redirect profit to creator if no supply
}
}
function _update(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts
) internal virtual override {
if (from != address(0)) saveProfit(from);
if (to != address(0)) saveProfit(to);
super._update(from, to, ids, amounts);
}
}

Exploit Example (Malicious Contract)

Here's how a malicious contract might exploit these vulnerabilities:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./VulnerableContract.sol";
contract MaliciousContract {
VulnerableContract public vulnerable;
constructor(VulnerableContract _vulnerable) {
vulnerable = _vulnerable;
}
function attack() public {
for (uint256 i = 0; i < 100; i++) {
vulnerable.claimProfit(); // Reentrancy attack
}
}
}

Mitigated Contract Code

Now, let's improve the contract by implementing proper gas optimization and reentrancy protection:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract SecureContract is ERC1155, AccessControl, ReentrancyGuard {
bytes32 public constant OWP_FACTORY_ROLE = keccak256("OWP_FACTORY_ROLE");
uint256 public totalProfit;
mapping(address => uint256) public lastProfit;
mapping(address => uint256) public savedProfit;
uint256 internal constant ACCURACY = 1e30;
uint256 internal constant MINIMUM_TRANSFER_AMOUNT = 10; // Minimum transfer value to prevent manipulation
// Define the event
event ProfitSaved(address indexed account, uint256 profit);
constructor() ERC1155("") {
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
_setupRole(OWP_FACTORY_ROLE, msg.sender);
}
function saveProfit(address account) internal returns (uint256 profit) {
uint256 unsaved = getUnsaved(account);
lastProfit[account] = totalProfit;
profit = savedProfit[account] + unsaved;
savedProfit[account] = profit;
// Emit the event
emit ProfitSaved(account, profit);
}
function getUnsaved(address account) internal view returns (uint256) {
return ((totalProfit - lastProfit[account]) * balanceOf(account, 0)) / ACCURACY;
}
function claimProfit() external nonReentrant returns (uint256 profit) {
profit = saveProfit(msg.sender);
require(profit > 0, "No profit available");
savedProfit[msg.sender] = 0;
IERC20(currency).safeTransfer(msg.sender, profit);
emit Claim(msg.sender, profit); // Existing event
emit ProfitSaved(msg.sender, profit); // New event
}
function sendProfit(uint256 amount) external onlyRole(OWP_FACTORY_ROLE) {
uint256 _totalSupply = totalSupply;
if (_totalSupply > 0) {
totalProfit += (amount * ACCURACY) / _totalSupply;
IERC20(currency).safeTransferFrom(msg.sender, address(this), amount);
emit Profit(amount);
} else {
IERC20(currency).safeTransferFrom(msg.sender, creator, amount); // Redirect profit to creator if no supply
}
}
function _update(
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts
) internal virtual override nonReentrant {
uint256 len = ids.length;
for (uint256 i = 0; i < len; i++) {
require(amounts[i] >= MINIMUM_TRANSFER_AMOUNT, "Transfer amount too low");
}
if (from != address(0)) saveProfit(from);
if (to != address(0)) saveProfit(to);
for (uint256 i = 0; i < len; i++) {
uint256 id = ids[i];
uint256 amount = amounts[i];
uint256 fromBalance = _balances[id][from];
uint256 toBalance = _balances[id][to];
_balances[id][from] = fromBalance - amount;
_balances[id][to] = toBalance + amount;
}
}
function transferTokens(address from, address to, uint256[] calldata ids, uint256[] calldata amounts) external onlyRole(OWP_FACTORY_ROLE) {
_update(from, to, ids, amounts);
}
}

Tools Used: VS code

Recommendations: Caching Array Lengths:

  • Description: Cache the length of the ids and amounts arrays to avoid repeated SLOAD operations in loops.

  • Implementation:

    solidity

    function _update(
    address from,
    address to,
    uint256[] memory ids,
    uint256[] memory amounts
    ) internal virtual override {
    uint256 len = ids.length; // Cache length
    for (uint256 i = 0; i < len; i++) {
    require(amounts[i] >= MINIMUM_TRANSFER_AMOUNT, "Transfer amount too low");
    }
    if (from != address(0)) saveProfit(from);
    if (to != address(0)) saveProfit(to);
    super._update(from, to, ids, amounts);
    }
  • Minimize State Writes:

    • Description: Reduce the number of storage writes by minimizing repeated lookups into storage.

    • Implementation:

      solidity

      function _update(
      address from,
      address to,
      uint256[] memory ids,
      uint256[] memory amounts
      ) internal virtual override {
      uint256 len = ids.length;
      for (uint256 i = 0; i < len; i++) {
      require(amounts[i] >= MINIMUM_TRANSFER_AMOUNT, "Transfer amount too low");
      }
      if (from != address(0)) saveProfit(from);
      if (to != address(0)) saveProfit(to);
      for (uint256 i = 0; i < len; i++) {
      uint256 id = ids[i];
      uint256 amount = amounts[i];
      uint256 fromBalance = _balances[id][from];
      uint256 toBalance = _balances[id][to];
      _balances[id][from] = fromBalance - amount;
      _balances[id][to] = toBalance + amount;
      }
      }
  • Use Calldata for Function Parameters:

    • Description: Use calldata for function parameters that are read-only, which is cheaper than memory.

    • Implementation:

      solidity

      function transferTokens(address from, address to, uint256[] calldata ids, uint256[] calldata amounts) external onlyRole(OWP_FACTORY_ROLE) {
      _update(from, to, ids, amounts);
      }
  • Event Logging for State Changes:

    • Description: Add events for state changes to make it easier to track and debug.

    • Implementation:

      solidity

      event ProfitClaimed(address indexed account, uint256 profit);
      event ProfitSaved(address indexed account, uint256 profit);

      In claimProfit and saveProfit functions:

      solidity

      function claimProfit() external returns (uint256 profit) {
      profit = saveProfit(msg.sender);
      require(profit > 0, "No profit available");
      savedProfit[msg.sender] = 0;
      IERC20(currency).safeTransfer(msg.sender, profit);
      emit Claim(msg.sender, profit); // Existing event
      emit ProfitClaimed(msg.sender, profit); // New event
      }
      function saveProfit(address account) internal returns (uint256 profit) {
      uint256 unsaved = getUnsaved(account);
      lastProfit[account] = totalProfit;
      profit = savedProfit[account] + unsaved;
      savedProfit[account] = profit;
      emit ProfitSaved(account, profit); // New event
      }
  • Limit Gas Usage in Loops:

    • Description: Avoid deep or unbounded loops to prevent excessive gas consumption, which could lead to transaction failures.

    • Implementation: Ensure any loops have a reasonable maximum length. If necessary, split operations across multiple transactions.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality
0xbrivan2 Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality

Support

FAQs

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