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
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);
currencyManager = new CurrencyManager();
testERC20 = new OWPERC20("Test Token", "TST");
membershipERC1155 = new MembershipERC1155();
membershipFactory = new MembershipFactory(
address(currencyManager),
address(oneWorldPlatformWalletAddress),
"https://baseURI/",
address(membershipERC1155)
);
currencyManager.addCurrency(address(testERC20));
deal(address(testERC20), DAOCreator, maxAmount);
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});
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 {
for (uint256 i = 0; i < 15000; i++) {
for (uint256 tier = 0; tier < 7; tier++) {
try handler.user_joins_dao(tier, i) {
} catch {
}
}
}
address[] memory addressesToBurn = new address[]();
for (uint256 i = 0; i < 10000; i++) {
addressesToBurn[i] = handler.actors(i);
}
vm.prank(address(membershipFactory));
vm.expectRevert();
membershipERC1155.burnBatchMultiple(addressesToBurn);
vm.stopPrank();
}
}
Now paste the following to Handler.sol
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;
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 membershipFactory.joinDAO(address(membershipERC1155), currentTier) {
return;
} catch {
triesToMint++;
}
}
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.