Platform: Code4rena
Start Date: 07/07/2023
Pot Size: $121,650 USDC
Total HM: 36
Participants: 111
Period: 7 days
Judge: Picodes
Total Solo HM: 13
Id: 258
League: ETH
Rank: 50/111
Findings: 5
Award: $193.51
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Udsen
Also found by: 0xMirce, 0xPsuedoPandit, 0xStalin, 0xbepresent, Aymen0909, Bobface, Co0nan, GREY-HAWK-REACH, Jeiwan, John, KupiaSec, LuchoLeonel1, Nyx, Praise, RedTiger, alexweb3, bin2chen, btk, dacian, dirk_y, josephdara, keccak123, ktg, mahdirostami, markus_ether, minhtrng, ni8mare, peanuts, ptsanev, ravikiranweb3, rvierdiiev, seeques, serial-coder, shaka, teawaterwire, wangxx2026, zzzitron
2.2492 USDC - $2.25
The intent of mintYieldFee
is to convert the shares that have been collected to the _yieldFeeRecipient
with internal accounting to be minted to the address of the assigned recipient. Instead, anyone can call mintYieldFee
and assign their address as recipient, taking the fees intended for _yieldFeeRecipient
.
mintYieldFee
has no access modifier and allows _recipient
to be set by the caller. The chosen _recipient
will receive shares minted to them, taking value from the Vault.
Manual review
Remove the _recipient
argument from mintYieldFee
. Modify the mint action to mint directly to _yieldFeeRecipient
which is set by the owner.
Access Control
#0 - c4-judge
2023-07-14T22:20:17Z
Picodes marked the issue as duplicate of #396
#1 - c4-judge
2023-08-05T22:04:48Z
Picodes marked the issue as satisfactory
🌟 Selected for report: wvleak
Also found by: 0xbepresent, 3docSec, DadeKuma, Jeiwan, Udsen, dirk_y, keccak123, rvierdiiev, serial-coder, shaka, wvleak
40.0854 USDC - $40.09
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1053 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1068
VaultHooks are a way for for winner to call an external smart contract when they win a prize. Any PoolTogether user can set a hook with setHooks
. _claimPrize
calls beforeClaimPrize
and afterClaimPrize
hooks of the VaultHooks for every winner. Because the individual winners have control over the address chosen for their own hooks, they can choose a malicious crafted contract that consumes large amounts of gas or reverts. A hook that disrupts the call will cause the user calling _claimPrize
to manually remove the offending winner, and if there are several winners with a problematic hook, the caller of _claimPrize
may spend extensive gas and time to call _claimPrize
, which can further reduce the incentive to assist the protocol with prize claims in the future.
The winner can selectively set the hook to cause the caller of _claimPrize
to waste time and gas, then later the winner can set the hook to a different contract and call _claimPrize
for their own address, meaning they will only disrupt protocol operations without losing out on their rewards.
Any user can call claimPrizes
to claim prizes on behalf of others. An array of winners is a function argument
https://github.com/GenerationSoftware/pt-v5-claimer/blob/57a381aef690a27c9198f4340747155a71cae753/src/Claimer.sol#L80
During the claiming process, the winner's beforeClaimPrize
and beforeClaimPrize
hooks will be called at the external contract specified by the winner
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1053
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L1068
The external contract can implement these hooks to prevent the for loop from completing, disrupting the caller's task of distributing rewards. These are example hooks that revert from an our of gas error or a revert.
function beforeClaimPrize( address winner, uint8 tier, uint32 prizeIndex ) external override returns (address) { uint a; while(true) { a += 1; } } function afterClaimPrize( address winner, uint8 tier, uint32 prizeIndex, uint256 payout, address recipient ) external override { revert dos(); }
Manual review
Documentation should clarify a process to be taken in cases where the hook set by the winner disrupts claiming operations. Consider adding a protocol incentive to prevent users from setting disruptive hooks, for example a 10% reduction from the original prize amount for every 24 hours that the winner has a disruptive hook set.Claimers could use a simulation process on a fork to confirm that the _claimPrize
process will complete without error, but a user who frontruns the process means this is not foolproof, especially if prizes are released at predictable times.
DoS
#0 - c4-judge
2023-07-18T20:00:14Z
Picodes marked the issue as duplicate of #465
#1 - c4-judge
2023-08-07T15:18:06Z
Picodes marked the issue as satisfactory
🌟 Selected for report: Jeiwan
Also found by: 0xSmartContract, 0xStalin, 3docSec, ABAIKUNANBAEV, btk, dev0cloo, dirk_y, grearlake, jaraxxus, keccak123, neumo, oxchsyston, rvierdiiev
22.9603 USDC - $22.96
Any ERC4626 vault has an underlying ERC20 representing the assets in the vault. setLiquidationPair
is an owner-controlled function that set a max approve to an owner-controlled address. This hands over control of the underlying assets to this external address, which can allow the owner to set their own malicious address and use transferFrom
to withdraw all assets from the Vault.
The max approve happens on this line
The liquidationPair_
address is set by the owner and there are no checks on this address besides confirming it is not the zero address. Once approved, a malicious owner can use their own malicious contract to withdraw all assets.
Manual review
Add some safeguards for depositors so that the Vault owner does not have complete control over the vault assets. This is not a simple fix with the existing value design. The new version of PoolTogether is intended to have greater decentralization, so limiting the owner's abilities to control crucial values is important.
Rug-Pull
#0 - c4-judge
2023-07-18T17:48:24Z
Picodes marked the issue as duplicate of #324
#1 - c4-judge
2023-08-06T10:46:23Z
Picodes marked the issue as satisfactory
🌟 Selected for report: bin2chen
Also found by: 0x11singh99, 0xWaitress, 0xbepresent, ABAIKUNANBAEV, ArmedGoose, Bauchibred, DadeKuma, GREY-HAWK-REACH, GalloDaSballo, Inspecktor, Jeiwan, Kaysoft, MohammedRizwan, Rolezn, Vagner, alexzoid, alymurtazamemon, ayden, banpaleo5, catellatech, dacian, erebus, eyexploit, fatherOfBlocks, grearlake, joaovwfreire, keccak123, kutugu, lanrebayode77, markus_ether, nadin, naman1778, rvierdiiev, squeaky_cactus, volodya, yixxas
15.9228 USDC - $15.92
liquidate
is the only function where _increaseYieldFeeBalance
is called. _increaseYieldFeeBalance
increases the value of _yieldFeeTotalSupply
. When _liquidationPair
calls liquidate
, the call can get frontrun by the contract owner calling _setYieldFeePercentage
, which changes the percentage of _amountOut
that is passed to _increaseYieldFeeBalance
and in doing directly changes the value of _yieldFeeTotalSupply
.
The call to _increaseYieldFeeBalance
, which increases the value of _yieldFeeTotalSupply
, takes place in liquidate
But setYieldFeePercentage
can be called at any time by the owner, such as by frontrunning, allowing the amount of value accumulated for the _yieldFeeRecipient
to be fully controlled by the owner.
Manual review
Create a new state variable _lastYieldUpdate
that stores the current block.timestamp
inside of setYieldFeePercentage
. A check can then be introduced in liquidate
to revert or take other actions if block.timestamp == _lastYieldUpdate
, which will prevent a user from becoming a victim of frontrunning from the contract owner.
MEV
#0 - c4-judge
2023-07-18T17:52:23Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2023-07-19T23:26:29Z
asselstine marked the issue as sponsor acknowledged
#2 - Picodes
2023-08-08T10:24:42Z
#3 - c4-judge
2023-08-08T10:24:50Z
Picodes changed the severity to QA (Quality Assurance)
#4 - c4-judge
2023-08-08T14:34:56Z
Picodes marked the issue as grade-b
🌟 Selected for report: 0xStalin
Also found by: 0xSmartContract, 0xWaitress, 3docSec, ABAIKUNANBAEV, DadeKuma, Jeiwan, K42, catellatech, dirk_y, keccak123, peanuts, squeaky_cactus
112.2875 USDC - $112.29
I really liked how there are few components that are concisely written in single contracts. This reduces the complexity of the system and makes it easier to understand the design and identify possible weaknesses. There were some parts of the Vault contract that made me raise an eyebrow. For example, _convertToShares
is implemented in one internal function but _convertToAssets
is implemented in two internal functions. The way that _currentExchangeRate
is used in _convertToShares
and _convertToAssets
is also a bit confusing, because the units and decimals of this value must be kept in mind everywhere that it is used. The relationship and overall flow with the yield vault was not fully clear to me, as was the intended goal of allowing anyone to be the owner of the vault.
To continue on this last point, will PoolTogether follow a similar model to yearn finance v3, where anyone can create strategies that are part of yearn, but yearn will vet the strategies and only feature vetted strategies on their main UI, with the other strategies found on something like ape.tax? Because this model is what makes sense to me given the existing design and desire for decentralization, but there will be continuous effort vetting new strategies, and I saw no documentation of the intention to do this.
I focused on effort on Vault.sol because this is where most of the funds pass through. I did not look at TwabController.sol with the time I allocated so I cannot comment on it.
8 hours
#0 - c4-judge
2023-07-18T18:48:30Z
Picodes marked the issue as grade-b
#1 - c4-sponsor
2023-07-20T20:49:56Z
asselstine marked the issue as sponsor confirmed