Platform: Code4rena
Start Date: 29/04/2022
Pot Size: $22,000 USDC
Total HM: 6
Participants: 40
Period: 3 days
Judge: Justin Goro
Total Solo HM: 2
Id: 114
League: ETH
Rank: 11/40
Findings: 3
Award: $603.76
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: WatchPug
Also found by: 0xDjango, CertoraInc, Tadashi, berndartmueller, kebabsec, leastwood, unforgiven
https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L232 https://github.com/pooltogether/aave-v3-yield-source/blob/e63d1b0e396a5bce89f093630c282ca1c6627e44/contracts/AaveV3YieldSource.sol#L361
A malicious, but generous, early depositor can DOS all future deposits. This is accomplished by directly sending aTokens to the AaveV3YieldSource.sol
contract after making their first deposit. The amount of aTokens sent to the contract will manipulate the share distribution for future depositors. Future depositors will need to deposit at least the amount of aTokens sent directly to the contract to receive even a single share.
The griefer could be assumed to be motivated by a desire to undermine the success of PoolTogether by DOSing contract interactions.
Steps to exploit:
1 wei
USDC1 wei
share based on the following formula:return _supply == 0 ? _tokens : _tokens.mul(_supply).div(aToken.balanceOf(address(this)));
Since supply==0, 1 wei shares is minted to the griefer.
aToken.balanceOf(address(this)
5,000 ether USDC
but the transaction will revert because of the following check: require(_shares > 0, "AaveV3YS/shares-gt-zero");
Calculation below without safeMath for ease of understanding:shares = 5,000 ether * 1 wei / 10,000 ether = 0 shares due to precision loss
Therefore, any depositor that attempts to deposit less than 10,000 ether USDC will be unable. Please note that 10,000 USDC is arbitrary. The griefer can send any value.
Finally, it should be noted that the AaveV3YieldSource contract does not have any way to transfer out aTokens to correct the issue.
Manual Review
Instead of relying on aToken.balanceOf(address(this)
, use internal accounting variables to keep track of the protocol's aToken balance.
In the transferERC20()
function, the owner is not allowed to send aTokens. Removing this restriction would help out in this situation.
#0 - PierrickGT
2022-05-03T16:03:48Z
This modifier checks that the input token is not aToken. The check is manually performed in the transferERC20()
function. If the purpose of the manual check is to provide a different require message, consider parameterizing the require message in the _requireNotAToken()
function.
#0 - PierrickGT
2022-05-03T16:49:11Z
🌟 Selected for report: IllIllI
Also found by: 0v3rf10w, 0x1f8b, 0x4non, 0xDjango, 0xf15ers, 0xkatana, 242, Dravee, GimelSec, MaratCerby, Tadashi, TrungOre, WatchPug, defsec, fatherOfBlocks, gzeon, hake, horsefacts, joestakey, miguelmtzinf, pauliax, pedroais, peritoflores, rotcivegaf, simon135, slywaters, tabish, throttle, z3s
34.6316 USDC - $34.63
Since the contract is compiled with Solidity version 0.8.10, aToken
can be declared as immutable and set in the constructor, similar to _decimals
. There is no setter function for the aToken address other than the constructor.
#0 - PierrickGT
2022-05-02T19:38:19Z