Platform: Code4rena
Start Date: 17/03/2022
Pot Size: $30,000 USDC
Total HM: 8
Participants: 43
Period: 3 days
Judge: gzeon
Total Solo HM: 5
Id: 100
League: ETH
Rank: 15/43
Findings: 2
Award: $231.15
π Selected for report: 0
π Solo Findings: 0
π Selected for report: defsec
Also found by: 0x1f8b, 0xDjango, 0xNazgul, 0xkatana, 0xwags, CertoraInc, Funen, GeekyLumberjack, GreyArt, IllIllI, Kenshin, Ruhum, TerrierLover, WatchPug, berndartmueller, bugwriter001, cccz, cmichel, csanuragjain, hake, kenta, kirk-baird, leastwood, minhquanym, oyc_109, peritoflores, rayn, remora, rfa, robee, saian, samruna, sorrynotsorry, wuwe1
202.7753 USDC - $202.78
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/DepositHook.sol#L14-L19
In DepositHook.sol
:
constructor(address _newAccessController, address _newDepositRecord) { _accountAccessController = IAccountAccessController( _newAccessController); _depositRecord = ICollateralDepositRecord(_newDepositRecord); }
There are no zero address checks in the constructor and no functions that allow for setting the addresses of _accountAccessController
and _depositRecord
. Therefore, if the wrong addresses were used, a whole new contract would have to be deployed to resolve the issue.
(Exact same finding at WithdrawHook.sol
regarding _depositRecord
)
Implement require()
functions in the constructor.
constructor(address _newAccessController, address _newDepositRecord) { require(_newAccessController != address(0), "Zero address for AccessController"); require( _newDepositRecord != address(0), "Zero address for DepositRecord"); _accountAccessController = IAccountAccessController(_newAccessController); _depositRecord = ICollateralDepositRecord(_newDepositRecord);
and/or
Implement a guarded setter function to change the parameters
_accountAccessController = IAccountAccessController(_newAccessController); function setDepositRecord(address _newDepositRecord)external onlyOwner { _depositRecord = ICollateralDepositRecord(_newDepositRecord); }
If implementing setter functions I also suggest implementing events in those.
No zero address check for setVault
(https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/DepositHook.sol#L39-L42).
In Collatereal.sol
(https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/Collateral.sol#L38-L50).
No zero address check for _baseToken
and _treasury
and no way to set new addresses for neither.
SingleStrategyController.sol
)Recommend to include the zero address check for _oldStrategy
in the constructor
and remove it from migrate()
as the event omitted from a migration from address(0)
would not contain meaningful information.
On a similar note I recommed including a zero address check using require()
in setVault()
.
DepositHook.sol
) - (CollateralDepositRecord.sol
):https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/DepositHook.sol#L26-L37
function hook( address _sender, uint256 _initialAmount, uint256 _finalAmount ) external override onlyVault { require( _accountAccessController.isAccountAllowed(_sender) && !_accountAccessController.isAccountBlocked(_sender), "Account not allowed to deposit" ); _depositRecord.recordDeposit(_sender, _finalAmount); }
Input _initialAmount
is never used.
My interpretation is that it is meant to be used in: _depositRecord.recordDeposit(_sender,_finalAmount)
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/CollateralDepositRecord.sol#L24-L39
So instead of:
function recordDeposit(address _sender, uint256 _amount) external override onlyAllowedHooks { require( _amount + _globalDepositAmount <= _globalDepositCap, "Global deposit cap exceeded" ); require( _amount + _accountToNetDeposit[_sender] <= _accountDepositCap, "Account deposit cap exceeded" ); _globalDepositAmount += _amount; _accountToNetDeposit[_sender] += _amount; }
We should have something like:
function recordDeposit(address _sender,uint256 _initialAmount, uint256 _finalAmount) external override onlyAllowedHooks { require( _initialAmount + _globalDepositAmount <= _globalDepositCap, "Global deposit cap exceeded" ); require( _finalAmount + _accountToNetDeposit[_sender] <= _accountDepositCap, "Account deposit cap exceeded" ); _globalDepositAmount += _initialAmount; _accountToNetDeposit[_sender] += _finalAmount; }
If that is not the case please remove _initalAmount
from the hook()
function.
Same things should be taken into consideration in hook()
from WithdrawHook.sol
, and recordWithdrawal()
in CollateralDepositRecord.sol
.
I also recommend including events for recordDeposit()
and recordWithdrawal()
.
DepositHook.sol
) - (AccountAccessController.sol
):https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/DepositHook.sol#L26-L37
function hook( address _sender, uint256 _initialAmount, uint256 _finalAmount ) external override onlyVault { require( _accountAccessController.isAccountAllowed(_sender) && !_accountAccessController.isAccountBlocked(_sender), "Account not allowed to deposit" ); _depositRecord.recordDeposit(_sender, _finalAmount); }
The two step require()
function checking if isAccountAllowed
could fail and an allowed account wouldnt go through if for example:
Account is allowed (https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/AccountAccessController.sol#L39-L48)
Account is blocked (https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/AccountAccessController.sol#L50-L59)
Account is allowed again (assuming accounts havent been cleared).
require()
fails because when an account is either blocked or allowed the function responsible doesnt remove the account from its counterpart index. So it can be true for both.
Remove && !_accountAccessController.isAccountBlocked(_sender)
from the require()
function and remove accounts from the allowed index if they are being blocked.
function blockAccounts(address[] calldata _accounts) external override onlyOwner { for (uint256 _i = 0; _i < _accounts.length; _i++) { _blockedAccounts[_blockedAccountsIndex][_accounts[_i]] = true; _allowedAccounts[_allowedAccountsIndex][_accounts[_i]] = false; emit AccountBlocked(_accounts[_i]); } }
Collateral.sol
)deposit()
and withdraw()
in Collateral.sol
.https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/Collateral.sol#L52-L93
Lines 62-65
// Record deposit before fee is taken if (address(_depositHook) != address(0)) { _depositHook.hook(msg.sender, _amount, _amountToDeposit); }
_depositHook
has to be run with every deposit (not optimal for gas).if()
statement wont revert. and the accessbility of accounts (allowed/blocked) wont be checked. So any blocked users will be able to deposit and withdraw assets freely, their transactions just wont be recorded by the accounting system. Same for withdraw()
.I suggest the zero address check for _depositHook
and withdrawHook
should rather be implemented at deploying or setting stage or at the start of the function at line 57 with a require()
function. That way if users are blocked they will never be able to deposit/withdraw and there wont be an accouting deficit.
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/Collateral.sol#L73-L79
Lines 73-79
_amountToDeposit -= _fee; uint256 _valueBefore = _strategyController.totalValue(); _baseToken.approve(address(_strategyController), _amountToDeposit); _strategyController.deposit(_amountToDeposit); uint256 _valueAfter = _strategyController.totalValue(); _amountToDeposit = _valueAfter - _valueBefore;
_amountToDeposit -= fee;
seems to be equal to _amountToDeposit = _valueAfter - _valueBefore;
. I believe the _valueAfter
and _valueBefore
calcualtions to be unnecessary.
Collateral.sol
)https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/Collateral.sol#L176-L184
This might lead to a few problems.
If by mistake _depositsAllowed
and _withdrawalsAllowed
are set to true before the _mintingFee
and/or _redemptionFee
are set, users can deposit and withdraw funds without paying any fees. Same is valid for totalSupply()
that might enable early users or front-runners to get a disproportionate amount of shares. If same users were to take advatange of flashloans this could cause large amounts of shares/rewards being paid out without any fees being collected.
Set require()
functions that ensure certain parameters are initialized before others.
E.g
bool _mintingFeeDeployed; bool _redemptionFeeDeployed; function setMintingFee(uint256 _newMintingFee) external override onlyOwner { require(_newMintingFee <= FEE_LIMIT, "Exceeds fee limit"); _mintingFeeDeployed = true; _mintingFee = _newMintingFee; emit MintingFeeChanged(_mintingFee); } function setRedemptionFee(uint256 _newRedemptionFee) external override onlyOwner { require(_newRedemptionFee <= FEE_LIMIT, "Exceeds fee limit"); _redemptionFeeDeployed = true; _redemptionFee = _newRedemptionFee; emit RedemptionFeeChanged(_redemptionFee); }
function setDepositsAllowed(bool _allowed) external override onlyOwner { require(_mintingFeeDeployed == true); require(_redemptionFeeDeployed == true); _depositsAllowed = _allowed; emit DepositsAllowedChanged(_allowed); } function setWithdrawalsAllowed(bool _allowed) external override onlyOwner { require(_mintingFeeDeployed == true); require(_redemptionFeeDeployed == true); _withdrawalsAllowed = _allowed; emit WithdrawalsAllowedChanged(_allowed); }
OR set these parameters at initialization and make sure there are require()
statements in place to ensure they are not wrongly set.
I believe the likelyhood of these parameters being set in the wrong order is low as they should be executed with care, but never the less still a possibility.
PrePOMarket.sol
)https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/PrePOMarket.sol#L121
No systems in place to ensure parameters are set in the right order (much like the examples given in Finding 5).
Lack of zero address check for _governance
and _collateral
and no setter function for _collateral
means that another contract would have to be deployed if inputs were wrong. I recommend implementing the zero address checks and/or setter functions.
PrePOMarketFactory.sol
)-(PrePOMarket.sol
)https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/PrePOMarket.sol#L44-L56
Constructor in PrePOMarket.sol
has 12 parameters with the last one being bool _allowed
.
When creating a _newMarket
in PrePOMarketFactory.sol
this last parameter is simply set to false
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/PrePOMarketFactory.sol#L63-L75
Change false
at line 75 in PrePOMarketFactory.sol
to allowed
for better readability.
#0 - ramenforbreakfast
2022-03-22T23:20:08Z
Finding 1 and 2 are duplicates of #4, but this one is better written/organized. Both are considered non issues by the team (zero address checks and initialAmount being unused) as stated in earlier mentions. Finding 3 This is more of a documentation problem. A blocked account does not necessarily mean that its corresponding entry in allowed needs to be unset and vice-versa. The account being blocked and allowed correctly results in a deposit being blocked. Finding 4 Is not an issue, we do not want to call a hook because not having one configured is a valid configuration. Finding 5 Is not an issue, zero fees are valid (and duplicated in many other issues) Finding 6 is a valid claim, but really minor Finding 7 is a style choice, that I disagree with.
While I think this is an very well written/organized submission, I think most of the issues here we do not consider to be valid. I would mark this as severity 0 however, because Finding 3 would not have been submitted if we more clearly explained the expected behavior. Finding 6 is valid but very minor.
28.3722 USDC - $28.37
https://github.com/code-423n4/2022-03-prepo/blob/main/contracts/core/AccountAccessController.sol#L15
constructor() {}
unused and should be removed.
Doing so will improve readability and save some gas.
#0 - ramenforbreakfast
2022-03-22T22:26:21Z
duplicate of #5 , also does not provide gas savings.
#1 - ramenforbreakfast
2022-04-13T20:48:26Z
@gzeoneth I am not sure why the duplicate label was removed here? This is mentioned exactly in #5 under Unnecessary Constructor.
Also this would make it an abstract
contract which is not our intention.
#2 - gzeoneth
2022-04-13T20:52:01Z
@ramenforbreakfast removing the duplicate tag is simply because how C4 score gas optimization reports. There are no duplicate concept as each of them will get a individual score.