Platform: Code4rena
Start Date: 03/07/2023
Pot Size: $100,000 USDC
Total HM: 4
Participants: 36
Period: 10 days
Judge: gzeon
Total Solo HM: 3
Id: 257
League: ETH
Rank: 6/36
Findings: 1
Award: $3,997.64
🌟 Selected for report: 0
🚀 Solo Findings: 0
3997.6359 USDC - $3,997.64
quit()
doesn't check erc20TokensToInclude
argument for repetitions (i.e. token address duplications), only checking the existence of a token in erc20TokensToIncludeInQuit
. Each time a token repeats the corresponding share of treasury holdings will be transferred to the quitting member.
The whole ERC20 holdings can be stolen by any forked DAO nouns holder.
Suppose Bob is the holder of 1
of the 50
forked nouns and stETH
is the only meaningful token on the balance, and it is included to erc20TokensToIncludeInQuit
. Bob can submit erc20TokensToInclude = [address(stETH), ..., address(stETH)]
(50 times in total), which will pass the check:
function quit(uint256[] calldata tokenIds, address[] memory erc20TokensToInclude) external nonReentrant { // check that erc20TokensToInclude is a subset of `erc20TokensToIncludeInQuit` address[] memory erc20TokensToIncludeInQuit_ = erc20TokensToIncludeInQuit; for (uint256 i = 0; i < erc20TokensToInclude.length; i++) { if (!isAddressIn(erc20TokensToInclude[i], erc20TokensToIncludeInQuit_)) { // @audit this check will be passed 50 times revert TokensMustBeASubsetOfWhitelistedTokens(); } } quitInternal(tokenIds, erc20TokensToInclude); }
And run quitInternal()
with this array, performing 50
transfers of 1/50
share each:
function quitInternal(uint256[] calldata tokenIds, address[] memory erc20TokensToInclude) internal { checkGovernanceActive(); uint256 totalSupply = adjustedTotalSupply(); for (uint256 i = 0; i < tokenIds.length; i++) { nouns.transferFrom(msg.sender, address(timelock), tokenIds[i]); } uint256[] memory balancesToSend = new uint256[](erc20TokensToInclude.length); // Capture balances to send before actually sending them, to avoid the risk of external calls changing balances. uint256 ethToSend = (address(timelock).balance * tokenIds.length) / totalSupply; for (uint256 i = 0; i < erc20TokensToInclude.length; i++) { IERC20 erc20token = IERC20(erc20TokensToInclude[i]); balancesToSend[i] = (erc20token.balanceOf(address(timelock)) * tokenIds.length) / totalSupply; } // Send ETH and ERC20 tokens timelock.sendETH(payable(msg.sender), ethToSend); for (uint256 i = 0; i < erc20TokensToInclude.length; i++) { if (balancesToSend[i] > 0) { timelock.sendERC20(msg.sender, erc20TokensToInclude[i], balancesToSend[i]); // @audit this will transfer 1/50 of the holdings 50 times } } emit Quit(msg.sender, tokenIds); }
A simple way to handle this is to add checkForDuplicates(erc20tokens)
check to the corresponding quit()
function:
function quit(uint256[] calldata tokenIds, address[] memory erc20TokensToInclude) external nonReentrant { + checkForDuplicates(erc20TokensToInclude); // check that erc20TokensToInclude is a subset of `erc20TokensToIncludeInQuit` address[] memory erc20TokensToIncludeInQuit_ = erc20TokensToIncludeInQuit; for (uint256 i = 0; i < erc20TokensToInclude.length; i++) { if (!isAddressIn(erc20TokensToInclude[i], erc20TokensToIncludeInQuit_)) { // @audit this check will be passed 50 times revert TokensMustBeASubsetOfWhitelistedTokens(); } } quitInternal(tokenIds, erc20TokensToInclude); }
Invalid Validation
#0 - c4-pre-sort
2023-07-14T20:28:56Z
0xSorryNotSorry marked the issue as low quality report
#1 - c4-pre-sort
2023-07-20T12:13:34Z
0xSorryNotSorry marked the issue as duplicate of #102
#2 - c4-judge
2023-07-24T09:06:27Z
gzeon-c4 marked the issue as satisfactory