Platform: Code4rena
Start Date: 22/07/2021
Pot Size: $80,000 USDC
Total HM: 6
Participants: 14
Period: 7 days
Judge: ghoulsol
Total Solo HM: 3
Id: 21
League: ETH
Rank: 4/14
Findings: 2
Award: $9,341.61
🌟 Selected for report: 9
🚀 Solo Findings: 0
🌟 Selected for report: pauliax
Also found by: eriksal1217
561.039 USDC - $561.04
pauliax
function withdraw in ILendingPool returns the actual withdrawn amount, however, function withdraw in AaveV2 strategy does not check this return value so e.g. function strategyWithdraw may actually withdraw less but still add the full amount to the staked balance: ps.strategy.withdraw(_amount); ps.stakeBalance = ps.stakeBalance.add(_amount);
function withdraw in IStrategy should return uint indicating the actual withdrawn amount and functions that use it should account for that.
#0 - Evert0x
2021-07-29T17:31:05Z
When looking at the LendingPool withdraw implementation: https://github.com/aave/protocol-v2/blob/master/contracts/protocol/lendingpool/LendingPool.sol#L142
It will revert if the _amount > balance. It basically only returns a different value then _amount in case it is uint256(-1), correct?
pauliax
initializeSherXERC20 sets the name and symbol of the SherX token. 'initialize' in the name indicates that it should be called only once. It can only be called by a contractOwner but still, I think it would be better to either prevent multiple initializations or rename function to remove this confusion.
Possible solutions:
#0 - Evert0x
2021-07-29T17:44:08Z
#116
🌟 Selected for report: pauliax
1246.7532 USDC - $1,246.75
pauliax
contract AaveV2 does not cache the lending pool, it retrieves it when necessary by calling a function getLp(). This is great as the implementation may change, however, this contract also approves an unlimited amount of want in the constructor: ILendingPool lp = getLp(); want.approve(address(lp), uint256(-1)); so if the implementation changes, the approval will reset. This will break the deposit function as it will try to deposit to this new lending pool with 0 approval.
For reference, function setLendingPoolImpl: https://github.com/aave/aave-protocol/blob/4b4545fb583fd4f400507b10f3c3114f45b8a037/contracts/configuration/LendingPoolAddressesProvider.sol#L58-L65
Not sure how likely is that lending pool implementation will change so marking this as 'Low'.
Before calling lp.deposit check that the approval is sufficient and increase otherwise.
🌟 Selected for report: pauliax
1246.7532 USDC - $1,246.75
pauliax
I think these checks should be inclusive: require(_unstakeWindow < 25000000, 'MAX'); require(_period < 25000000, 'MAX'); if (_amount > oldValue) // >= will reduce gas here
require(_unstakeWindow <= 25000000, 'MAX'); require(_period <= 25000000, 'MAX'); if (_amount >= oldValue)
🌟 Selected for report: pauliax
1246.7532 USDC - $1,246.75
pauliax
In Base struct having 3 separate fields that map from _protocol is error-prone. If you later introduce new fields, etc, you need not forget to delete them in function protocolRemove, etc. I think it would be better to have a separate struct for protocol-related data and map to that.
An example solution, replace: mapping(bytes32 => address) protocolManagers; mapping(bytes32 => address) protocolAgents; mapping(bytes32 => bool) protocolIsCovered; with: struct ProtocolInfo { address manager; address agent; bool covered; } struct Base { ... mapping(bytes32 => ProtocolInfo) protocolInfo; ... } Then you can delete all fields this way: delete gs.protocolInfo[_protocol]; Similar solution may be applied to PoolStorage (protocolBalance, protocolPremium, isProtocol).
🌟 Selected for report: pauliax
1246.7532 USDC - $1,246.75
pauliax
I see no re-entrancy mitigations. Contracts interact with various outside sources (tokens, aave pools, other possible strategies that may be added in the future, etc). so, for instance, now you have to be careful and do not allow tokens that have a receiver callback (e.g. erc777) or untrustable sources of yield (strategies).
Consider using ReentrancyGuard on main action functions: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol
🌟 Selected for report: pauliax
1246.7532 USDC - $1,246.75
pauliax
When the address has no unstake entries function getInitialUnstakeEntry still returns 0 index. This function is external but can still confuse the outside consumers.
Consider requiring ps.unstakeEntries[_staker].length > 0;
🌟 Selected for report: pauliax
1246.7532 USDC - $1,246.75
pauliax
Probably you are aware of this, but as I see many for loops throughout the code iterating over dynamic arrays I suggest being very careful as the execution may exceed the block gas limit, consume all the gas provided, and fail. Some arrays have removal functions, but there is, for instance, unstakeEntries array that is never actually removed as 'delete ps.unstakeEntries[msg.sender][_id];' only resets the values to default.
You can consider introducing max limits on items in the arrays or make sure that elements can be removed from dynamic arrays in case it becomes too large.
30.1784 USDC - $30.18
pauliax
Variables in contract AaveV2 can be marked as immutable (constant) as their values do not change. All of the variables are eligible: lpAddressProvider, aaveIncentivesController, want, aWant, sherlock, aaveLmReceiver. Immutable greatly reduces gas costs.
Add the 'immutable' or 'constant' keyword to the declarations of variables that are only set once.
#0 - Evert0x
2021-07-29T12:00:53Z
#1
41.9145 USDC - $41.91
pauliax
There are some massive structs. Gas efficiency can be achieved by tightly packing the struct. Struct variables are stored in 32 bytes each so you can group smaller types to occupy less storage. You can read more here: https://fravoll.github.io/solidity-patterns/tight_variable_packing.html or in the official documentation: https://docs.soliditylang.org/en/v0.4.21/miscellaneous.html
It is hard to predict which order is the most gas efficient as, for example, library PoolStorage struct Base contains over 20 members with all different types. It will need several iterations of changing and testing to find an optimal order (with the help of gasReporter).
#0 - Evert0x
2021-07-29T17:33:28Z
#85
103.4926 USDC - $103.49
pauliax
.length in a loop can be extracted into a variable and used where necessary to reduce the number of storage reads. Examples where this is relevant: for (uint256 i = 0; i < ps.protocols.length; i++) for (uint256 i; i < gs.tokensStaker.length; i++) for (uint256 i; i < gs.tokensSherX.length; i++) Cache the length of the array and use this local variable when iterating over the storage array. For instance, function calcUnderlying reads gs.tokensSherX.length multiple times before so a cache variable should significantly reduce gas costs.
uint protocolsLength = ps.protocols.length; for (uint256 i = 0; i < protocolsLength; i++) and so on
#0 - Evert0x
2021-07-29T17:34:46Z
#86
🌟 Selected for report: pauliax
229.9835 USDC - $229.98
pauliax
There is no difference between functions aBalance and balanceOf in contract AaveV2, they both return aWant, so there is no point in having them separately.
Remove internal function aBalance and make balanceOf public.
🌟 Selected for report: pauliax
229.9835 USDC - $229.98
pauliax
Call to LibDiamond.contractOwner() can be cached here: require(msg.sender == LibDiamond.contractOwner(), 'NOT_DEV'); require(_govDev != LibDiamond.contractOwner(), 'SAME_DEV');
address contractOwner = LibDiamond.contractOwner(); require(msg.sender == contractOwner , 'NOT_DEV'); require(_govDev != contractOwner, 'SAME_DEV');