Platform: Code4rena
Start Date: 31/01/2023
Pot Size: $90,500 USDC
Total HM: 47
Participants: 169
Period: 7 days
Judge: LSDan
Total Solo HM: 9
Id: 211
League: ETH
Rank: 49/169
Findings: 4
Award: $308.30
🌟 Selected for report: 1
🚀 Solo Findings: 0
159.3877 USDC - $159.39
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L666-L670 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L448 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L608 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L621 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L634 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L647
Modifier VaultController._verifyCreatorOrOwner
does not work. Instead of checking the condition msg.sender is creator OR owner
, it makes msg.sender is creator AND owner
. This would block access to all created Vaults for creators and the owner (if he did not create them).
Specifically, the following functions in the VaultController
are affected:
addStakingRewardsTokens()
;deployVault()
, which has a call to addStakingRewardsTokens()
, cannot be executed if the argument rewardsData.length != 0
;pauseAdapters()
;pauseVaults()
;unpauseAdapters()
;unpauseVaults()
.To check this concept, we can make a truth table for the main condition in the modifier msg.sender != metadata.creator || msg.sender != owner
. The table shows that the condition will equal false
only in the one case where msg.sender
is both creator and owner.
msg.sender != metadata.creator | msg.sender != owner | msg.sender != metadata.creator || msg.sender != owner |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
The correct condition should be the following: msg.sender != metadata.creator && msg.sender != owner
.
msg.sender != metadata.creator | msg.sender != owner | msg.sender != metadata.creator && msg.sender != owner |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
In this case, a revert will only happen when msg.sender
is neither a creator nor the owner, as it should be according to the documentation.
You can also use the following test to check; add it to the file test\vault\VaultController.t.sol
:
function testFail__deployVault_creator_is_not_owner_audit() public { addTemplate("Adapter", templateId, adapterImpl, true, true); addTemplate("Strategy", "MockStrategy", strategyImpl, false, true); addTemplate("Vault", "V1", vaultImpl, true, true); controller.setPerformanceFee(uint256(1000)); controller.setHarvestCooldown(1 days); rewardToken.mint(bob, 10 ether); rewardToken.approve(address(controller), 10 ether); swapTokenAddresses[0] = address(0x9999); address adapterClone = 0xD6C5fA22BBE89db86245e111044a880213b35705; address strategyClone = 0xe8a41C57AB0019c403D35e8D54f2921BaE21Ed66; address stakingClone = 0xE64C695617819cE724c1d35a37BCcFbF5586F752; uint256 callTimestamp = block.timestamp; vm.prank(bob); address vaultClone = controller.deployVault( VaultInitParams({ asset: iAsset, adapter: IERC4626(address(0)), fees: VaultFees({ deposit: 100, withdrawal: 200, management: 300, performance: 400 }), feeRecipient: feeRecipient, owner: bob }), DeploymentArgs({id: templateId, data: abi.encode(uint256(100))}), DeploymentArgs({id: "MockStrategy", data: ""}), address(0), abi.encode( address(rewardToken), 0.1 ether, 1 ether, true, 10000000, 2 days, 1 days ), VaultMetadata({ vault: address(0), staking: address(0), creator: bob, metadataCID: metadataCid, swapTokenAddresses: swapTokenAddresses, swapAddress: address(0x5555), exchange: uint256(1) }), 0 ); }
In the test's log (forge test --match-test "testFail__deployVault_creator_is_not_owner" -vvvv
), you can see that the call ended with revert NotSubmitterNorOwner(0x000000000000000000000000000000000000000000000000DCbA)
.
Manual Review, VSCodium, Forge
Change the condition to msg.sender != metadata.creator && msg.sender != owner
.
#0 - c4-judge
2023-02-16T07:23:21Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2023-02-17T10:11:51Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-sponsor
2023-02-18T12:08:12Z
RedVeil marked the issue as disagree with severity
#3 - c4-judge
2023-02-23T01:21:15Z
dmvt marked the issue as selected for report
🌟 Selected for report: immeas
Also found by: KIntern_NA, bin2chen, minhtrng, rbserver, rvierdiiev, ustas, yellowBirdy
69.3758 USDC - $69.38
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L472-L477 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L479-L494 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L496-L499
The performanceFee
logic in Vault
allows users to evade it. To do this, recalculate highWaterMark
before calling takeManagementAndPerformanceFees()
which is responsible for issuing owner commissions.
Recalculation of highWaterMark
is possible with a call to a function with syncFeeCheckpoint
modifier:
deposit()
mint()
withdraw()
In one of the scenarios, users can front-run every transaction to takeManagementAndPerformanceFees()
.
Use the following test to check; add it to the file test\vault\VaultController.t.sol
:
function test__evadePerformanceFee_audit() public { uint256 assetAmount = 1 ether; uint256 depositAmount = 1 ether; _setFees(0, 0, 0, 1e17); // Deposit some funds to the vault asset.mint(alice, depositAmount + 1); vm.startPrank(alice); asset.approve(address(vault), depositAmount); vault.deposit(depositAmount, alice); vm.stopPrank(); // highWaterMark is equal to standard value assertEq(vault.highWaterMark(), 1e18); assertEq(vault.accruedPerformanceFee(), 0); // Increase balance of the adapter to trigger performanceFee asset.mint(address(adapter), assetAmount); assertEq(vault.highWaterMark(), 1e18); assertEq(vault.accruedPerformanceFee(), 1e17); // Withdraw minimal amount to trigger syncFeeCheckpoint modifier vm.prank(alice); vault.withdraw(1); // highWaterMark is updated assertEq(vault.highWaterMark(), 2e18 - 1); // Fee is now equal to 0 assertEq(vault.accruedPerformanceFee(), 0); }
Manual Review, VSCodium, Forge
Review of the fees logics.
#0 - c4-judge
2023-02-16T06:11:21Z
dmvt marked the issue as duplicate of #30
#1 - c4-sponsor
2023-02-18T12:05:21Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T00:13:57Z
dmvt marked the issue as satisfactory
#3 - c4-judge
2023-02-23T01:13:13Z
dmvt changed the severity to 2 (Med Risk)
🌟 Selected for report: rbserver
Also found by: bin2chen, cccz, chaduke, eccentricexit, hansfriese, ustas
44.0481 USDC - $44.05
The Vault
contract contains two functions for swap shares for funds:
withdraw(uint256 assets, ...)
redeem(uint256 shares, ...)
It is assumed that the difference between these functions is only in the arguments. In withdraw()
, we specify how many final assets
we want to get, and in redeem()
, we specify how many shares
to burn.
But redeem()
, unlike withdraw()
, has no syncFeeCheckpoint
modifier, which allows the user to perform the same action (withdrawal) with different consequences, thus manipulating the performanceFee
commission.
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L204-L240 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L246-L278 https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L496-L499
Manual Review, VSCodium, Forge
Apply the syncFeeCheckpoint
modifier to the redeem()
.
#0 - c4-judge
2023-02-16T02:49:47Z
dmvt marked the issue as primary issue
#1 - c4-sponsor
2023-02-17T07:45:32Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-judge
2023-02-23T11:27:27Z
dmvt marked issue #557 as primary and marked this issue as a duplicate of 557
#3 - c4-judge
2023-02-23T11:28:09Z
dmvt marked the issue as partial-50
🌟 Selected for report: IllIllI
Also found by: 0x3b, 0xAgro, 0xBeirao, 0xMirce, 0xNineDec, 0xRobocop, 0xSmartContract, 0xTraub, 0xWeiss, 2997ms, 41i3xn, Awesome, Aymen0909, Bauer, Bnke0x0, Breeje, Cryptor, DadeKuma, Deathstore, Deekshith99, DevABDee, DevTimSch, Dewaxindo, Diana, Ermaniwe, Guild_3, H0, IceBear, Inspectah, JDeryl, Kaiziron, Kaysoft, Kenshin, Mukund, Praise, RaymondFam, Rickard, Rolezn, Ruhum, Sathish9098, SkyWalkerMan, SleepingBugs, UdarTeam, Udsen, Walter, aashar, adeolu, apvlki, arialblack14, ast3ros, btk, chaduke, chandkommanaboyina, chrisdior4, climber2002, codetilda, cryptonue, cryptostellar5, csanuragjain, ddimitrov22, descharre, dharma09, doublesharp, eccentricexit, ethernomad, fs0c, georgits, halden, hansfriese, hashminer0725, immeas, lukris02, luxartvinsec, matrix_0wl, merlin, mookimgo, mrpathfindr, nadin, olegthegoat, pavankv, rbserver, rebase, savi0ur, sayan, scokaf, seeu, shark, simon135, tnevler, tsvetanovv, ulqiorra, ustas, waldenyan20, y1cunhui, yongskiws, yosuke
35.4779 USDC - $35.48
The contract code quality is average, and the documentation is incomplete and often confusing.
@notice
states that the function's "caller must be owner", but there is no onlyOwner
modifier, and it must be open (check the docs).
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/DeploymentController.sol#L59-L72
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/README.md?plain=1#L33changeVaultAdapters()
and changeVaultFees()
in the VaultController
contract, it is possible to call fallback
or selector functions changeAdapter()
and changeFees()
from any contract in the network without any checks on behalf of AdminProxy
. It possibly can be a medium-severity issue.
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L331-L344
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L368-L381@notice
states that the function's "caller must be owner", but there is canCreate
modifier instead.
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L79
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L181
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/VaultController.sol#L274@dev
states that there are 525600 minutes per year, but the function uses SECONDS_PER_YEAR
constants, which is 365.25 days
or 525960 minutes.
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L422-L439
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L35block.timestamp
is, by default, added to event information. It is redundant to add it as an additional field.
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L536
https://github.com/code-423n4/2023-01-popcorn/blob/d95fc31449c260901811196d617366d6352258cd/src/vault/Vault.sol#L585#0 - c4-judge
2023-02-28T14:55:43Z
dmvt marked the issue as grade-b