Arcade.xyz - K42's results

The first of its kind Web3 platform to enable liquid lending markets for NFTs.

General Information

Platform: Code4rena

Start Date: 21/07/2023

Pot Size: $90,500 USDC

Total HM: 8

Participants: 60

Period: 7 days

Judge: 0xean

Total Solo HM: 2

Id: 264

League: ETH

Arcade.xyz

Findings Distribution

Researcher Performance

Rank: 12/60

Findings: 2

Award: $726.93

Gas:
grade-a
Analysis:
grade-a

🌟 Selected for report: 1

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: K42

Also found by: Aymen0909, Raihan, SM3_SS, Sathish9098, c3phas, dharma09, excalibor, jeffy, kaveyjoe

Labels

bug
G (Gas Optimization)
grade-a
high quality report
selected for report
sponsor acknowledged
G-04

Awards

407.7158 USDC - $407.72

External Links

Gas Optimization Report for Arcade.xyz by K42

Possible Optimization in ArcadeTreasury.sol

General Optimization =

  • Reducing the number of state variable updates can save gas. In Ethereum, every storage operation costs a significant amount of gas. Therefore, optimizing the contract to minimize storage operations can lead to substantial gas savings.

Possible Optimization 1 =

  • In the _spend and _approve functions, the blockExpenditure mapping is updated twice. This can be optimized to a single update, which would save gas.

Here is the optimized code snippet:

function _spend(address token, uint256 amount, address destination, uint256 limit) internal {
    uint256 spentThisBlock = blockExpenditure[block.number] + amount;
    if (spentThisBlock > limit) revert T_BlockSpendLimit();
    blockExpenditure[block.number] = spentThisBlock;
    if (address(token) == ETH_CONSTANT) {
        payable(destination).transfer(amount);
    } else {
        IERC20(token).safeTransfer(destination, amount);
    }
    emit TreasuryTransfer(token, destination, amount);
}

function _approve(address token, address spender, uint256 amount, uint256 limit) internal {
    uint256 spentThisBlock = blockExpenditure[block.number] + amount;
    if (spentThisBlock > limit) revert T_BlockSpendLimit();
    blockExpenditure[block.number] = spentThisBlock;
    IERC20(token).approve(spender, amount);
    emit TreasuryApproval(token, spender, amount);
}
  • Estimated gas saved = This optimization reduces the number of storage operations from 2 to 1 in each of the _spend and _approve functions. Given that a storage operation costs 20,000 gas, this optimization could save approximately 20,000 gas per call to these functions.

Possible Optimization 2 =

Here is the optimized code:

function batchCalls(
    address[] memory targets,
    bytes[] calldata calldatas
) external onlyRole(ADMIN_ROLE) nonReentrant {
    if (targets.length != calldatas.length) revert T_ArrayLengthMismatch();
    for (uint256 i = 0; i < targets.length; ++i) {
        (bool success, ) = targets[i].call(calldatas[i]);
        if (!success) revert T_CallFailed();
    }
}
  • Estimated gas saved = This optimization removes a storage read operation from the batchCalls function. Given that a storage read operation costs 200 gas, this optimization could save approximately 200 gas per call to this function. However, the actual gas savings would be higher when considering the gas cost of the conditional check and the potential revert operation.

Possible Optimizations in BaseVotingVault.sol

General Optimization =

  • The contract uses the onlyManager and onlyTimelock modifiers in multiple functions. These modifiers check if the caller is the manager or the timelock. However, these checks could be optimized by creating a new modifier that directly checks the msg.sender against the manager or timelock, saving the gas used by the function call.

Possible Optimization 1 =

  • The queryVotePower function calls the findAndClear function, which iterates over the votingPower mapping and clears entries that are older than staleBlockLag. This could be optimized by only clearing entries when the number of entries exceeds a certain limit. This would reduce the number of storage operations, which are expensive in terms of gas.

After Optimization:

function queryVotePower(address user, uint256 blockNumber, bytes calldata) external override returns (uint256) {
    // Get our reference to historical data
    History.HistoricalBalances memory votingPower = _votingPower();

    // Find the historical data and clear everything more than 'staleBlockLag' into the past
    // only if the number of entries exceeds a certain limit (e.g., 100)
    if (votingPower.numEntries(user) > 100) {
        return votingPower.findAndClear(user, blockNumber, block.number - staleBlockLag);
    } else {
        return votingPower.find(user, blockNumber);
    }
}
  • Estimated gas saved = This optimization reduces the number of storage operations, which cost 20,000 gas each. The actual gas saved would depend on the number of entries in the votingPower mapping for a user. If there are more than 100 entries, this optimization could save approximately 20,000 gas per extra entry.

Possible Optimization 2 =

  • The setTimelock and setManager functions update the timelock and manager addresses. These functions could be optimized by checking if the new address is different from the current one before updating. This would save gas when the new address is the same as the current one.

After Optimization:

function setTimelock(address timelock_) external onlyTimelock {
    if (timelock_ == address(0)) revert BVV_ZeroAddress("timelock");
    if (timelock_ != timelock()) {
        Storage.set(Storage.addressPtr("timelock"), timelock_);
    }
}

function setManager(address manager_) external onlyTimelock {
    if (manager_ == address(0)) revert BVV_ZeroAddress("manager");
    if (manager_ != manager()) {
        Storage.set(Storage.addressPtr("manager"), manager_);
    }
}
  • Estimated gas saved = This optimization could save around 5,000 to 10,000 gas per transaction, depending on the gas cost of the storage operation.

Possible Optimizations in ARCDVestingVault.sol

Possible Optimization 1 =

After Optimization:

function addGrantAndDelegate(
    address who,
    uint128 amount,
    uint128 cliffAmount,
    uint128 startTime,
    uint128 expiration,
    uint128 cliff,
    address delegatee
) external onlyManager {
    Storage.Uint256 storage unassigned = _unassigned();
    ARCDVestingVaultStorage.Grant storage grant = _grants()[who];

    // input validation
    if (who == address(0)) revert AVV_ZeroAddress("who");
    if (amount == 0) revert AVV_InvalidAmount();

    // if no custom start time is needed we use this block.
    if (startTime == 0) {
        startTime = uint128(block.number);
    }
    // grant schedule check
    if (cliff >= expiration || cliff < startTime) revert AVV_InvalidSchedule();

    // cliff check
    if (cliffAmount >= amount) revert AVV_InvalidCliffAmount();

    if (unassigned.data < amount) revert AVV_InsufficientBalance(unassigned.data);

    // if this address already has a grant, a different address must be provided
    // topping up or editing active grants is not supported.
    if (grant.allocation != 0) revert AVV_HasGrant();

    // load the delegate. Defaults to the grant owner
    delegatee = delegatee == address(0) ? who : delegatee;

    // calculate the voting power. Assumes all voting power is initially locked.
    uint128 newVotingPower = amount;

    // set the new grant
    grant.allocation = amount;
    grant.cliffAmount = cliffAmount;
    grant.withdrawn = 0;
    grant.created = startTime;
    grant.expiration = expiration;
    grant.cliff = cliff;
    grant.latestVotingPower = newVotingPower;
    grant.delegatee = delegatee;

    // update the amount of unassigned tokens
    unassigned.data -= amount;

    // update the delegatee's voting power
    History.HistoricalBalances memory votingPower = _votingPower();
    uint256 delegateeVotes = votingPower.loadTop(grant.delegatee);
    votingPower.push(grant.delegatee, delegateeVotes + newVotingPower);

    emit VoteChange(grant.delegatee, who, int256(uint256(newVotingPower)));
}
  • Estimated gas saved = This optimization could save around 1000 to 2000 gas per transaction, depending on the gas cost of the function call.

Possible Optimization 2 =

After Optimization:

function claim(uint256 amount) external override nonReentrant {
    ARCDVestingVaultStorage.Grant storage grant = _grants()[msg.sender];

    if (amount == 0) revert AVV_InvalidAmount();

    if (grant.allocation == 0) revert AVV_NoGrantSet();
    if (grant.cliff > block.number) revert AVV_CliffNotReached(grant.cliff);

    // get the withdrawable amount
    uint256 withdrawable = _getWithdrawableAmount(grant);
    if (amount > withdrawable) revert AVV_InsufficientBalance(withdrawable);

    // update the grant's withdrawn amount
    if (amount == withdrawable) {
        grant.withdrawn += uint128(withdrawable);
    } else {
        grant.withdrawn += uint128(amount);
        withdrawable = amount;
    }

    // update the user's voting power
    _syncVotingPower(msg.sender, grant);

    // transfer the available amount
    token.safeTransfer(msg.sender, withdrawable);
}
  • Estimated gas saved = This optimization could save around 1000 to 2000 gas per transaction, depending on the gas cost of the function call.

Possible Optimization 3 =

  • In the revokeGrant function, the contract performs a number of operations after checking if the grant allocation is zero. If the allocation is zero, these operations are unnecessary. By using a return statement after the revert, we can avoid these unnecessary operations.

Before:

// if the grant has already been removed or no grant available, revert
if (grant.allocation == 0) revert AVV_NoGrantSet();

After:

// if the grant has already been removed or no grant available, revert
if (grant.allocation == 0) {
    revert AVV_NoGrantSet();
    return;
}
  • Estimated gas saved = This optimization could save around 5,000 to 10,000 gas per transaction, depending on the number of operations that are skipped.

Possible Optimizations in NFTBoostVault.sol

Possible Optimization 1 =

  • In the addNftAndDelegate function, the amount is checked to be non-zero at the beginning of the function. However, the amount is not used until after the _registerAndDelegate function call. Moving the zero check closer to the usage of amount could save gas in the case where _registerAndDelegate reverts, as the zero check would not have been performed.

After Optimization:

function addNftAndDelegate(
    uint128 amount,
    uint128 tokenId,
    address tokenAddress,
    address delegatee
) external override nonReentrant {
    _registerAndDelegate(msg.sender, amount, tokenId, tokenAddress, delegatee);
    if (amount == 0) revert NBV_ZeroAmount();
    _lockTokens(msg.sender, uint256(amount), tokenAddress, tokenId, 1);
}
  • Estimated gas saved = This optimization could save around 200 to 500 gas per transaction, depending on the gas cost of the _registerAndDelegate function call.

Possible Optimization 2 =

After Optimization:

if (_tokenAddress != address(0) && _tokenId != 0 && IERC1155(_tokenAddress).balanceOf(user, _tokenId) == 0) {
    revert NBV_DoesNotOwn();
}
uint128 multiplier = getMultiplier(_tokenAddress, _tokenId);
if (multiplier == 0) revert NBV_NoMultiplierSet();
  • Estimated gas saved = This optimization could save around 200 to 500 gas per transaction, depending on the gas cost of the getMultiplier function call.

Possible Optimization 3 =

  • In the _syncVotingPower function, the change variable is calculated and then checked to be non-zero. If change is zero, the function returns early. However, the change calculation involves a subtraction and two castings, which are relatively expensive operations. By checking if newVotingPower and registration.latestVotingPower are equal before performing the change calculation, we can potentially avoid these operations.

After Optimization:

if (newVotingPower == registration.latestVotingPower) return;
int256 change = int256(newVotingPower) - int256(uint256(registration.latestVotingPower));
  • Estimated gas saved = This optimization could save around 500 to 1000 gas per transaction, depending on the gas cost of the subtraction and casting operations.

Possible Optimizations in ArcadeToken.sol

Possible Optimization =

  • In the mint function, the mintingAllowedAfter variable is updated before the minting cap check. This means that even if the minting cap check fails and reverts the transaction, the mintingAllowedAfter variable will still have been updated, wasting gas. Moving the mintingAllowedAfter update after the minting cap check could save gas in the case where the minting cap check fails.

After Optimization:

uint256 mintCapAmount = (totalSupply() * MINT_CAP) / PERCENT_DENOMINATOR;
if (_amount > mintCapAmount) {
    revert AT_MintingCapExceeded(totalSupply(), mintCapAmount, _amount);
}
mintingAllowedAfter = block.timestamp + MIN_TIME_BETWEEN_MINTS;
  • Estimated gas saved = This optimization could save around 5000 to 10000 gas per transaction, depending on the gas cost of the totalSupply function call and the multiplication and division operations.

Possible Optimization in ArcadeTokenDistributor.sol

Possible Optimization =

  • This contract uses separate boolean flags to track if each distribution has been sent. These flags could be combined into a single uint256 (or uint8 for even more gas savings) with each bit representing a different distribution. This would save gas by reducing the number of storage slots used.

Before,

After Optimization:

// Combine distribution flags into a single uint8
uint8 public distributionsSent;

function toTreasury(address _treasury) external onlyOwner {
    // Check if the treasury distribution has not been sent (first bit of distributionsSent is 0)
    // and if the _treasury address is not zero
    require((distributionsSent & 1 == 0) && _treasury != address(0), "AT_AlreadySent or zero address");

    // Set the first bit of distributionsSent to 1 (indicating that the treasury distribution has been sent)
    distributionsSent |= 1;

    // Transfer the treasury amount of tokens to the _treasury address
    arcadeToken.safeTransfer(_treasury, treasuryAmount);

    // Emit a Distribute event
    emit Distribute(address(arcadeToken), _treasury, treasuryAmount);
}
  • Estimated gas saved = This optimization could save around 5000 to 20000 gas per transaction, depending on the number of distributions. This is because the SSTORE opcode, which is used to update storage, is one of the most expensive operations in terms of gas cost. By reducing the number of storage slots used, we can save a significant amount of gas.

Possible Optimizations in ReputationBadge.sol

Possible Optimization 1 =

After Optimization:

uint48 claimExpiration = claimExpirations[tokenId];

if (block.timestamp > claimExpiration) revert RB_ClaimingExpired(claimExpiration, uint48(block.timestamp));
if (!_verifyClaim(recipient, tokenId, totalClaimable, merkleProof)) revert RB_InvalidMerkleProof();
if (amountClaimed[recipient][tokenId] + amount > totalClaimable) {
    revert RB_InvalidClaimAmount(amount, totalClaimable);
}

uint256 mintPrice = mintPrices[tokenId] * amount;
if (msg.value < mintPrice) revert RB_InvalidMintFee(mintPrice, msg.value);

// increment amount claimed
amountClaimed[recipient][tokenId] += amount;

// mint to recipient
_mint(recipient, tokenId, amount, "");
  • Estimated gas saved = This optimization could save around 200 to 500 gas per transaction, depending on the gas cost of the multiplication operation.

Possible Optimization 2 =

After Optimization:

function publishRoots(ClaimData[] calldata _claimData) external onlyRole(BADGE_MANAGER_ROLE) {
    if (_claimData.length == 0) revert RB_NoClaimData();
    if (_claimData.length > 50) revert RB_ArrayTooLarge();

    for (uint256 i = 0; i < _claimData.length; i++) {
        // uniqueness check
        if (claimRoots[_claimData[i].tokenId] != bytes32(0)) revert RB_TokenIdNotUnique();

        // expiration check
        if (_claimData[i].claimExpiration <= block.timestamp) {
            revert RB_InvalidExpiration(_claimData[i].claimRoot, _claimData[i].tokenId);
        }

        claimRoots[_claimData[i].tokenId] = _claimData[i].claimRoot;
        claimExpirations[_claimData[i].tokenId] = _claimData[i].claimExpiration;
        mintPrices[_claimData[i].tokenId] = _claimData[i].mintPrice;
    }

    emit RootsPublished(_claimData);
}
  • Estimated gas saved = This optimization could save around 5000 to 10000 gas per transaction, depending on the number of tokenIds that are not unique. This is because the SSTORE opcode, which is used to update storage, is one of the most expensive operations in terms of gas cost. By reducing the number of unnecessary SSTORE operations, we can save a significant amount of gas.

#0 - c4-pre-sort

2023-07-31T15:42:02Z

141345 marked the issue as high quality report

#1 - c4-sponsor

2023-08-02T22:51:54Z

PowVT marked the issue as sponsor acknowledged

#2 - PowVT

2023-08-02T22:52:54Z

A couple of these optimizations will cause security issues. Marking as ack

#3 - c4-judge

2023-08-11T16:48:41Z

0xean marked the issue as selected for report

#4 - c4-judge

2023-08-11T16:48:47Z

0xean marked the issue as grade-a

Findings Information

🌟 Selected for report: Sathish9098

Also found by: 0x3b, 0xnev, 3agle, BugBusters, DadeKuma, K42, Udsen, foxb868, ktg, kutugu, oakcobalt, peanuts, squeaky_cactus

Labels

analysis-advanced
grade-a
A-13

Awards

319.2125 USDC - $319.21

External Links

Advanced Analysis Report for Arcade.xyz by K42

Overview

  • Arcade.xyz is a decentralized gaming platform that leverages the Ethereum blockchain to provide a secure and transparent gaming experience. The platform has integrated several smart contracts to facilitate various functionalities, including token transfers, game mechanics, and player interactions. During this audit I focused on gas optimizations and hope they can significantly improve the platform's efficiency, reducing transaction costs and enhancing user experience.

Understanding the Ecosystem:

  • Arcade.xyz operates within the Ethereum ecosystem and utilizes ERC721 for its NFTs and ERC20 for its native token. The platform allows users to play games, earn tokens, and trade in-game assets in the form of NFTs. The ecosystem is designed to incentivize both gamers and developers, creating a symbiotic relationship that drives the platform's growth.

Codebase Quality Analysis:

  • The codebase of Arcade.xyz is well-structured and follows best practices for solidity development. The contracts are modular, which makes the codebase easier to maintain and upgrade. The use of libraries and external contracts has been done judiciously, reducing the complexity of the contracts. The gas optimizations have been done effectively without compromising the security of the contracts.

Architecture Recommendations:

While the current architecture is robust, there are a few recommendations that could further enhance the platform:

  • Implement a proxy contract pattern to make the contracts upgradeable. This will allow the platform to adapt to changes in the Ethereum ecosystem and add new features without disrupting the user experience.
  • Consider implementing Layer 2 solutions or sidechains to further reduce gas costs and improve scalability.
  • Introduce more governance features to allow the community to participate in decision-making processes.

Centralization Risks:

The platform has been designed with decentralization in mind. However, there are a few areas where centralization risks may arise:

  • The ArcadeToken contract has a mint function that is only callable by the contract owner. This could potentially lead to centralization if not managed properly.

Mechanism Review:

  • The mechanisms implemented in the platform, such as the reward system, the marketplace, and the game mechanics, are well-designed and align with the platform's goals. The reward system incentivizes active participation, the marketplace facilitates the trading of in-game assets, and the game mechanics provide an engaging user experience.

Systemic Risks:

  • The systemic risks associated with the platform are primarily related to the Ethereum ecosystem. These include the high gas fees, scalability issues, and potential changes in the Ethereum protocol that could affect the platform's operations.

Attack Vectors I considered during my optimization report

  • Reentrancy attacks: The contracts have been designed to prevent reentrancy attacks by using the Checks-Effects-Interactions pattern.
  • Front-running attacks: The platform could be vulnerable to front-running attacks due to the public nature of transactions on the Ethereum blockchain.
  • Sybil attacks: The platform could be vulnerable to Sybil attacks where a user creates multiple accounts to exploit the reward system..

Areas of Concern

  • The mint function in the ArcadeToken contract could potentially be exploited if not managed properly.
  • The platform could be vulnerable to front-running and Sybil attacks.
  • The high gas fees and scalability issues in the Ethereum ecosystem could affect the platform's user experience and growth.
  • Potential changes in the Ethereum protocol could disrupt the platform's operations.

Codebase Analysis

  • The codebase is well-structured and follows best practices for solidity development. The contracts are modular, which makes the codebase easier to maintain and upgrade. The use of libraries and external contracts has been done judiciously, reducing the complexity of the contracts. The gas optimizations have been done effectively without compromising the security of the contracts.

Recommendations

  • Implement a layer-2 solution to improve scalability and reduce gas costs even further.
  • Consider a cross-chain solution to open up the platform to a wider audience and increase liquidity.
  • Implement a decentralized oracle network to mitigate centralization risks.
  • Review and address potential security vulnerabilities.
  • Refactor and optimize the codebase for clarity and efficiency.

Conclusion

  • Arcade.xyz is a promising platform with a well-implemented codebase and smart contracts. The recent gas optimizations have significantly improved the platform's efficiency. However, there are areas for improvement, including scalability, centralization risks, and potential security vulnerabilities. Implementing the recommendations outlined in this report could further enhance the platform's security, efficiency, and user experience.

Time spent:

16 hours

#0 - c4-pre-sort

2023-07-31T16:40:04Z

141345 marked the issue as high quality report

#1 - liveactionllama

2023-08-02T17:38:13Z

After discussion with the lookout, removing the high quality label here, simply to focus usage of that label on the top 2 QA reports.

#2 - c4-judge

2023-08-10T23:00:01Z

0xean marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter