Project

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

burnBatchMultiple function will revert due to gas limit in large number of addresses

Summary

https://github.com/Cyfrin/2024-11-one-world/blob/1e872c7ab393c380010a507398d4b4caca1ae32b/contracts/dao/tokens/MembershipERC1155.sol#L91

The burnBatchMultiple function in the MembershipERC1155 contract reverts when processing large batch operations due to excessive gas consumption. This vulnerability restricts functionality when handling high-volume token burns, as it causes out-of-gas reverts under normal usage.

Vulnerability Details

The function burnBatchMultiple attempts to process a batch burn for a large array of addresses (addressesToBurn). When provided with a significant number of addresses, the cumulative gas cost exceeds the block gas limit, leading to a revert due to gas exhaustion.

The issue was observed during testing when attempting to burn tokens across a batch of 10,000 addresses. Even under optimized conditions, the gas required exceeded what is feasible in a single transaction.

POC

Steps to reproduce: Install Foundry to the project, create the following folder structure test/foundry/invariants. Inside of that folder, create 2 files:

-BurnBatchMultipleOutOfGas.t.sol

-Handler.t.sol

Paste the following in BurnBatchMultipleOutOfGas.t.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test, console} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {MembershipERC1155} from "../../../contracts/dao/tokens/MembershipERC1155.sol";
import {MembershipFactory} from "../../../contracts/dao/MembershipFactory.sol";
import {CurrencyManager} from "../../../contracts/dao/CurrencyManager.sol";
import {OWPERC20} from "../../../contracts/shared/testERC20.sol";
import {DAOInputConfig, DAOType, TierConfig} from "../../../contracts/dao/libraries/MembershipDAOStructs.sol";
import "forge-std/Test.sol";
import {Handler} from "./Handler.t.sol";
interface IERC1155 {
function balanceOf(address account, uint256 id) external view returns (uint256);
}
contract burnBatchMultiple_gasLimit is StdInvariant, Test {
Handler handler;
CurrencyManager public currencyManager;
MembershipFactory public membershipFactory;
OWPERC20 public testERC20;
address DAOCreator = makeAddr("DAOCreator");
address oneWorldPlatformWalletAddress = makeAddr("oneWorldPlatformWalletAddress");
MembershipERC1155 public membershipERC1155;
uint256 maxAmount = type(uint256).max;
address public createdDAOAddress;
address owner = makeAddr("owner");
function setUp() public {
vm.startPrank(owner);
//deploy currency manager
currencyManager = new CurrencyManager();
//deploy test token
testERC20 = new OWPERC20("Test Token", "TST");
//deploy membershipterc1155 implementation only
membershipERC1155 = new MembershipERC1155();
//deploy membershipfactory
membershipFactory = new MembershipFactory(
address(currencyManager),
address(oneWorldPlatformWalletAddress),
"https://baseURI/",
address(membershipERC1155)
);
//whitelist test token from currentcyManager
currencyManager.addCurrency(address(testERC20));
deal(address(testERC20), DAOCreator, maxAmount);
//create sponsored dao
DAOInputConfig memory daoConfig = DAOInputConfig({
ensname: "testdao.eth",
daoType: DAOType.SPONSORED,
currency: address(testERC20),
maxMembers: 15000,
noOfTiers: 7
});
TierConfig[] memory tierConfigs = new TierConfig[]();
tierConfigs[0] = TierConfig({amount: 6000, price: 6400, power: 64, minted: 0});
tierConfigs[1] = TierConfig({amount: 3000, price: 3200, power: 32, minted: 0});
tierConfigs[2] = TierConfig({amount: 2000, price: 1600, power: 16, minted: 0});
tierConfigs[3] = TierConfig({amount: 1500, price: 800, power: 8, minted: 0});
tierConfigs[4] = TierConfig({amount: 1000, price: 400, power: 4, minted: 0});
tierConfigs[5] = TierConfig({amount: 900, price: 200, power: 2, minted: 0});
tierConfigs[6] = TierConfig({amount: 600, price: 100, power: 1, minted: 0});
// Call the function
createdDAOAddress = membershipFactory.createNewDAOMembership(daoConfig, tierConfigs);
console.log("createdDAOAddress", createdDAOAddress);
vm.stopPrank();
handler = new Handler(currencyManager, membershipFactory, testERC20, MembershipERC1155(createdDAOAddress));
bytes4[] memory selectors = new bytes4[]();
selectors[0] = handler.user_joins_dao.selector;
targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
targetContract(address(handler));
}
function test_burnBatchMultiple_gasLimit_outOfGas() public {
// First mint tokens to many addresses using existing logic
for (uint256 i = 0; i < 15000; i++) {
for (uint256 tier = 0; tier < 7; tier++) {
try handler.user_joins_dao(tier, i) {
// Successfully minted
} catch {
// Failed to mint in this tier, continue to next
}
}
}
// we will burn 10k out of 15k
address[] memory addressesToBurn = new address[]();
for (uint256 i = 0; i < 10000; i++) {
addressesToBurn[i] = handler.actors(i);
}
// Try to burn all - this should revert with gas limit
vm.prank(address(membershipFactory));
vm.expectRevert();
membershipERC1155.burnBatchMultiple(addressesToBurn);
vm.stopPrank();
}
}

Now paste the following to Handler.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.22;
import {Test, console} from "forge-std/Test.sol";
import {MembershipERC1155} from "../../../contracts/dao/tokens/MembershipERC1155.sol";
import {MembershipFactory} from "../../../contracts/dao/MembershipFactory.sol";
import {CurrencyManager} from "../../../contracts/dao/CurrencyManager.sol";
import {OWPERC20} from "../../../contracts/shared/testERC20.sol";
import {DAOInputConfig, DAOType, TierConfig} from "../../../contracts/dao/libraries/MembershipDAOStructs.sol";
import "forge-std/Test.sol";
contract Handler is Test {
CurrencyManager currencyManager;
MembershipFactory membershipFactory;
OWPERC20 testERC20;
MembershipERC1155 membershipERC1155;
address owner;
address profitSender = makeAddr("profitSender");
address[] public actors;
address internal currentActor;
uint256 maxAmount = type(uint256).max;
constructor(
CurrencyManager _currencyManager,
MembershipFactory _membershipFactory,
OWPERC20 _testERC20,
MembershipERC1155 _membershipERC1155
) {
currencyManager = _currencyManager;
membershipFactory = _membershipFactory;
testERC20 = _testERC20;
membershipERC1155 = _membershipERC1155;
// Initialize some test actors
for (uint256 i = 0; i < 15000; i++) {
actors.push(address(uint160(0x50 + i)));
}
}
modifier useActor(uint256 actorIndexSeed) {
currentActor = actors[bound(actorIndexSeed, 0, actors.length - 1)];
deal(address(testERC20), currentActor, maxAmount);
vm.startPrank(currentActor);
testERC20.approve(address(membershipFactory), maxAmount);
_;
vm.stopPrank();
}
function user_joins_dao(uint256 _tier, uint256 actorIndexSeed) public useActor(actorIndexSeed) {
_tier = bound(_tier, 0, 6);
uint256 triesToMint = 0;
while (triesToMint < 7) {
uint256 currentTier = (_tier + triesToMint) % 7;
// Try to mint at the current tier
try membershipFactory.joinDAO(address(membershipERC1155), currentTier) {
return; // Successfully joined
} catch {
// If joining fails (tier is full), try next tier
triesToMint++;
}
}
// If we get here, we couldn't join any tier
return;
}
}

Run forge test -vvvv --match-contract burnBatchMultiple_gasLimit .

You will get a result similar to this:

Ran 1 test suite in 3.88s (1.82s CPU time): 1 tests passed, 1 failed, 0 skipped (2 total tests)
Failing tests:
Encountered 1 failing test in test/foundry/invariants/BurnBatchMultipleOutOfGas.t.sol:burnBatchMultiple_gasLimit
[FAIL: EvmError: OutOfGas] test_burnBatchMultiple_gasLimit_outOfGas() (gas: 1073720760)
Encountered a total of 1 failing tests, 1 tests succeeded

Impact

This vulnerability hinders the usability of the burnBatchMultiple function for any scenario requiring large batch burns. Token administrators and users cannot rely on this function to handle extensive batch operations, creating scalability limitations and potential administrative bottlenecks in token lifecycle management.

Tools Used

Foundry, manual review

Recommendations

You might consider implementing a mechanism to handle smaller batch sizes within the burnBatchMultiple function, limiting the number of addresses processed in each transaction to ensure gas efficiency.

Updates

Lead Judging Commences

0xbrivan2 Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!