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: 65/169
Findings: 3
Award: $146.06
🌟 Selected for report: 0
🚀 Solo Findings: 0
22.4778 USDC - $22.48
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L143-L145
This lead to the loss of feeShare that is supposed to be minted to the feeRecipient in case of that some amount of assets that is less than certain threshold would be deposited into the vault.
Within the Vault# deposit()
, the feeShares
is calculated like this:
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L143-L145
/** * @notice Deposit exactly `assets` amount of tokens, issuing vault shares to `receiver`. * @param assets Quantity of tokens to deposit. * @param receiver Receiver of issued vault shares. * @return shares Quantity of vault shares issued to `receiver`. */ function deposit(uint256 assets, address receiver) public nonReentrant whenNotPaused syncFeeCheckpoint returns (uint256 shares) { if (receiver == address(0)) revert InvalidReceiver(); uint256 feeShares = convertToShares( assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down) /// @audit - The "feeShares" is calculated here. ); shares = convertToShares(assets) - feeShares; if (feeShares > 0) _mint(feeRecipient, feeShares); /// @audit - If "feeShares" is less than 0, the shares based on the fee is not minted to feeRecipient
However, there is no input validation for the assets
parameter in the the Vault# deposit()
above.
Thus, any amount of assets
are deposited via the assets
parameter in the the Vault# deposit()
.
If some small amount of assets
that is less than certain threshold would be deposited, the feeShares
to be minted will be 0
.
Example). Let's say:
1e3
)fees.deposit
is 1 BPS ( 1e14
)
The calculation by using above is like this:uint256 feeShares = convertToShares(assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down)) uint256 feeShares = convertToShares(1e3 * 1e14 / 1e18) uint256 feeShares = 0
Like the calculation above, the result of 1e3 * 1e14 / 1e18
will be assigned as the argument value of the convertToShares()
function.
Due to the rounding down in Solidity, the result of 1e3 * 1e14 / 1e18
will be 0
. Thus, convertToShares(1e3 * 1e14 / 1e18)
will return 0
. As a result, the feeShares
will be 0
.
Manual review
Consider adding the input validation for checking wether or not the amount of assets
is more than threshold to the line that is before the calculation of feeShares
is executed like this:
function deposit(uint256 assets, address receiver) public nonReentrant whenNotPaused syncFeeCheckpoint returns (uint256 shares) { if (receiver == address(0)) revert InvalidReceiver(); if (assets * uint256(fees.deposit) < 1e18) revert InvalidAssetsAmount(); /// @audit - Adding this validation to here. uint256 feeShares = convertToShares( assets.mulDiv(uint256(fees.deposit), 1e18, Math.Rounding.Down) );
#0 - c4-judge
2023-02-16T07:57:59Z
dmvt marked the issue as duplicate of #71
#1 - c4-sponsor
2023-02-18T12:13:56Z
RedVeil marked the issue as sponsor confirmed
#2 - c4-sponsor
2023-02-18T12:14:08Z
RedVeil marked the issue as disagree with severity
#3 - c4-judge
2023-02-23T00:53:35Z
dmvt marked the issue as partial-50
#4 - c4-judge
2023-02-23T01:13:57Z
dmvt changed the severity to 2 (Med Risk)
88.0962 USDC - $88.10
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/interfaces/vault/IVault.sol#L8-L13 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/interfaces/vault/IVault.sol#L22 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L106 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L132 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L60 https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L88
This lead to a bad situation that the invalid rates of fees (i.e. 1000% (1e19)) will be applied to each fees unless the fee will be changed. (NOTE:It takes 2 days at least in order to change the rate of fees) During these invalid rates of fees is applied to each fees, if a user deposit the assets to the Vault, the assets of user will be lost or its transaction will always fail.
Within the IVault.sol, the VaultFees
struct is defined like below:
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/interfaces/vault/IVault.sol#L8-L13
// Fees are set in 1e18 for 100% (1 BPS = 1e14) struct VaultFees { uint64 deposit; uint64 withdrawal; uint64 management; uint64 performance; }
fees
is defined in the form of the VaultFees
struct within the VaultInitParams
struct like below:
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/interfaces/vault/IVault.sol#L22
/// @notice Init data for a Vault struct VaultInitParams { /// @Notice Address of the deposit asset IERC20 asset; /// @Notice Address of the adapter used by the vault IERC4626 adapter; /// @Notice Fees used by the vault VaultFees fees; /// @audit /// @Notice Address of the recipient of the fees address feeRecipient; /// @Notice Owner of the vault (Usually the submitter) address owner; }
When new Vault would be deployed, the VaultController.sol# _deployVault()
is called within the VaultController.sol# deployVault()
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L106
function deployVault( VaultInitParams memory vaultData, DeploymentArgs memory adapterData, DeploymentArgs memory strategyData, address staking, bytes memory rewardsData, VaultMetadata memory metadata, uint256 initialDeposit ) external canCreate returns (address vault) { ... vault = _deployVault(vaultData, _deploymentController); /// @audit ...
Within the VaultController.sol# _deployVault()
, new Vault would be deployed with the vaultData
in the form of the VaultInitParams struct including the fees
in the form of the VaultFee struct above via the Vault# initialize()
function like below:
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/VaultController.sol#L132
/// @notice Deploys a new vault contract using the `activeTemplateId`. function _deployVault(VaultInitParams memory vaultData, IDeploymentController _deploymentController) internal returns (address vault) { vaultData.owner = address(adminProxy); (bool success, bytes memory returnData) = adminProxy.execute( address(_deploymentController), abi.encodeWithSelector( DEPLOY_SIG, VAULT, activeTemplateId[VAULT], abi.encodeWithSelector(IVault.initialize.selector, vaultData) /// @audit - New Vault would be deployed with the "vaultData" in the form of the VaultInitParams struct including the fees in the form of the VaultFee struct via the Vault# `initialize()` function ) ); ... }
Within the Vault# initialize()
, the fees_
in the form of the VaultFees
struct would be assigned as a parameter.
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L60
/** * @notice Initialize a new Vault. * ... * @param fees_ Desired fees in 1e18. (1e18 = 100%, 1e14 = 1 BPS) * ... */ function initialize( IERC20 asset_, IERC4626 adapter_, VaultFees calldata fees_, /// @audit address feeRecipient_, address owner ) external initializer {
However, there is no input validations for the fees_
in the form of the VaultFees struct until the fees_
would be assigned into the variable of the fees
within the Vault# initialize()
above:
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L88
fees = fees_; /// @audit
This allow the owner (deployer) to assign more than 100% (1e18)
into the each fees by mistake. (i.e. 1000% (1e19))
Once these invalid rates of fees is set by mistake, the owner needs to wait for proposedFeeTime + quitPeriod
(NOTE:According to the condition within the setQuitPeriod() function, quitPeriod
is 2 days at least) to change the rates of each fees via the both functions the proposeFees()
and the changeFees()
https://github.com/code-423n4/2023-01-popcorn/blob/main/src/vault/Vault.sol#L541
function changeFees() external { if (block.timestamp < proposedFeeTime + quitPeriod). /// @audit revert NotPassedQuitPeriod(quitPeriod);
This lead to a bad situation that the invalid rates of fees (i.e. 1000% (1e19)) will be applied to each fees unless the fee will be changed and it takes 2 days at least like above. During these invalid rates of fees is applied to each fees, if a user deposit the assets to the Vault, the assets of user will be lost or its transaction will always fail.
Manual review
Consider adding an input validation for the fee_
before it's assigned into the variable of fee
within the Vault# initialize()
like this:
function initialize( IERC20 asset_, IERC4626 adapter_, VaultFees calldata fees_, /// @audit address feeRecipient_, address owner ) external initializer { ... /// @audit - Consider adding the input validation for the "fee_" before it's assigned into the variable of "fee" like this: if ( fees_.deposit >= 1e18 || fees_.withdrawal >= 1e18 || fees_.management >= 1e18 || fees_.performance >= 1e18 ) revert InvalidVaultFees(); fees = fees_; ...
#0 - c4-judge
2023-02-16T04:19:08Z
dmvt marked the issue as duplicate of #198
#1 - c4-sponsor
2023-02-18T12:00:08Z
RedVeil marked the issue as disagree with severity
#2 - c4-sponsor
2023-02-18T12:00:12Z
RedVeil marked the issue as sponsor confirmed
#3 - c4-judge
2023-02-23T16:19:44Z
dmvt changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-02-23T22:28:22Z
dmvt marked the issue as satisfactory