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: 3/14
Findings: 3
Award: $11,670.34
🌟 Selected for report: 9
🚀 Solo Findings: 0
shw
When a user stakes or a protocol deposits a transfer-on-fee/deflationary token, the solution does not correctly handle the received amount, which could be less than what is accounted for.
Referenced code: PoolOpen.sol#L36-L38 PoolBase.sol#L270-L271
Get the actual received amount by calculating the difference of token balance before and after the transfer. For example, re-writing line 36-38 of PoolOpen
as follows:
uint256 balanceBefore = _token.balanceOf(address(this)); _token.safeTransferFrom(msg.sender, address(this), _amount); uint256 receivedAmount = _token.balanceOf(address(this)) - balanceBefore; lock = LibPool.stake(ps, receivedAmount, _receiver);
#0 - Evert0x
2021-07-31T08:32:33Z
#12
🌟 Selected for report: hickuphh3
Also found by: eriksal1217, shw
shw
The Gov
contract uses the SafeERC20
library to handle the transfer of tokens that are not compliant with the ERC20 specification. However, in line 300, the approve
function is used instead of the safeApprove
function. Tokens not compliant with the ERC20 specification could return false
from the approve
call to indicate the approval fails, while the calling contract would not notice the failure if the return value is not checked.
Referenced code: Gov.sol#L300
Use safeApprove
instead of approve
, which reverts the transaction with a proper error message when the return value of approve
is false
. A better approach is to use safeIncreaseAllowance
instead, which mitigates the multiple withdrawal attack on ERC20 tokens.
#0 - Evert0x
2021-07-31T08:15:53Z
#51
shw
The decreaseApproval
method of SherXERC20
is considered unsafe since the allower has no trivial way to know whether the spender transferred any number of tokens from him before a decreaseApproval
call is executed. Please refer to the following link for more details.
Referenced code: SherXERC20.sol#L79-L83
OpenZeppelin/openzeppelin-contracts - Method decreaseApproval
in unsafe
Consider using increaseAllowance
and decreaseAllowance
instead. Besides, increaseApproval
and decreaseApproval
are replaced in the OpenZeppelin library in this commit.
#0 - Evert0x
2021-07-31T08:30:13Z
#117
#1 - ghoul-sol
2021-08-30T00:48:22Z
Duplicate of #117 so low risk
🌟 Selected for report: shw
1246.7532 USDC - $1,246.75
shw
The calcUnderlyingInStoredUSD()
function of SherX
should return calcUnderlyingInStoredUSD(getSherXBalance())
instead of calcUnderlyingInStoredUSD(sx20.balances[msg.sender])
since there could be SherX unallocated to the user at the time of the function call. A similar function, calcUnderlying()
, calculates the user's underlying tokens based on the user's current balance plus the unallocated ones.
Referenced code: SherX.sol#L141
Change sx20.balances[msg.sender]
to getSherXBalance()
at line 141.
#0 - Evert0x
2021-07-31T08:40:24Z
1 (low risk); as the function is called '..inStored..', at it is using the stored variables. I agree it is a confusing function name.
#1 - ghoul-sol
2021-08-30T00:50:34Z
agree with sponsor, low risk
shw
When withdrawing funds from the strategy
, the PoolStrategy
assumes that the exact amount
of want
tokens is withdrawn and increases the stake balance by amount
. However, this may not always be true, depending on the strategy or the yield source of the strategy. The actual return number of tokens could be less than the provided parameter amount
, thus resulting in an inaccurate stake balance. (For example, some vaults return only their total balance when the desired withdrawal amount exceeds it.)
Referenced code: PoolStrategy.sol#L79-L80
Consider modifying the withdraw
function of IStrategy
to return a uint
indicating the actual withdraw amount from the yield source. Take AaveV2
as an example, directly return the result from Aave (line 95), which is the actual withdraw amount according to Aave's documentation.
#0 - Evert0x
2021-07-31T08:42:17Z
#44
#1 - Evert0x
2021-07-31T08:42:55Z
However, this may not always be true, depending on the strategy or the yield source of the strategy.
True, but currently only the AaveV2 strategy is used.
#2 - ghoul-sol
2021-08-30T01:13:46Z
Duplicate of #44 so low risk
shw
The transferFrom
function of SherXERC20
checks the _from
address to be non-zero, while the _to
address is not checked. Neither the _from
address nor the _to
address is checked for the' transfer' function.
Referenced code: SherXERC20.sol#L105 SherXERC20.sol#L94-L97
Consider adding non-zero address checks for the _from
and _to
addresses in the internal _transfer
function instead of the transfer
or transferFrom
functions.
#0 - Evert0x
2021-07-31T08:05:49Z
#118
🌟 Selected for report: shw
1246.7532 USDC - $1,246.75
shw
SafeMath library functions are not always used in arithmetic operations in the PoolBase
contract, which could potentially cause integer underflow/overflows. Although in the reference lines of code, there are upper limits on the variables to ensure an integer underflow/overflow could not happen, using SafeMath is always a best practice, which prevents underflow/overflows completely (even if there were no assumptions on the variables) and increases code consistency as well.
Referenced code: PoolBase.sol#L136 PoolBase.sol#L325 PoolBase.sol#L344 PoolBase.sol#L362 PoolBase.sol#L364
Consider using the SafeMath library functions in the referenced lines of code.
🌟 Selected for report: shw
1246.7532 USDC - $1,246.75
shw
A possible divide-by-zero error could happen in the getSherXPerBlock(uint256, IERC20)
function of PoolBase
when the totalSupply
of lockToken
and _lock
are both 0.
Referenced code: PoolBase.sol#L215
Check if baseData().lockToken.totalSupply().add(_lock)
equals to 0 before line 214. If so, then return 0.
🌟 Selected for report: shw
1246.7532 USDC - $1,246.75
shw
Adding non-zero address checks on the following function's parameters can help ensure the ownership of contracts is not lost or the contracts do not need to be redeployed if any of them is provided as zero accidentally.
Referenced code: GovDev.sol#L19-L23 NativeLock.sol#L19 ForeignLock.sol#L20
Consider adding non-zero address checks on the parameters.
#0 - Evert0x
2021-07-31T08:07:19Z
GovDev.sol#L19-L23
is used to eventually renounce the role, but maybe it makes sense to create a different function for that.
🌟 Selected for report: shw
1246.7532 USDC - $1,246.75
shw
The getInitialUnstakeEntry
function of PoolBase
returns the first active unstaking entry of a staker, which requires the current block to be strictly before the last block in the unstaking window. However, the unstake
function allows the current block to be exactly the same as the last block (same logic in unstakeWindowExpiry
).
Referenced code: PoolBase.sol#L136 PoolBase.sol#L344 PoolBase.sol#L364
Change the <=
comparison at line 136 to <
for consistency.
30.1784 USDC - $30.18
shw
Declaring state variables in the AaveV2
contract as constant
or immutable
(whose value is assigned only at the construction time) can save gas.
Can be declared as constant
:
AaveV2.sol#L24
Can be declared as immutable
:
AaveV2.sol#L26
AaveV2.sol#L29
AaveV2.sol#L31-L32
Add a constant
or immutable
keyword in the referenced lines of code.
#0 - Evert0x
2021-07-31T08:46:00Z
#1
🌟 Selected for report: shw
1246.7532 USDC - $1,246.75
shw
A token cannot be reinitialized with a new lock token once it is set to a non-zero address. If the lock token needs to be changed (for example, because of implementation errors), the token must be removed and added again.
Referenced code: Gov.sol#L218-L227
Consider removing the if
condition at line 219 to allow the lock token to be reinitialized.
#0 - Evert0x
2021-07-31T08:28:37Z
Upgrading the lockToken a pretty complex procedure. As old lockTokens suddenly become worthless.
103.4926 USDC - $103.49
shw
In general, declaring dynamic function parameters as calldata
instead of memory
could save gas since the parameters are not copied into the memory but directly read from the call data.
Referenced code: Gov.sol#L138 Gov.sol#L165 Manager.sol#L46 Manager.sol#L74-L75 Manager.sol#L95-L97 Manager.sol#L142-L144 Manager.sol#L166 Manager.sol#L168 Manager.sol#L203-L206 Payout.sol#L110-L113 SherX.sol#L193-L194 SherXERC20.sol#L53
Consider changing the memory
keyword to calldata
in the referenced functions. However, two of them require local variables to be rearranged or scoped to avoid "stack too deep" compiler errors. Please refer to this link for the detailed implementation.
#0 - Evert0x
2021-07-31T08:43:41Z
#87
🌟 Selected for report: shw
229.9835 USDC - $229.98
shw
In the PoolStorage
library, declaring the POOL_STORAGE_PREFIX
constant with type bytes32
, and change abi.encode
ti abi.encodePacked
at line 87 can save gas.
Referenced code: PoolStorage.sol#L14 PoolStorage.sol#L87
Related links:
Change string
to byteX
if possible
Solidity-Encode-Gas-Comparison
See above
🌟 Selected for report: shw
229.9835 USDC - $229.98
shw
A storage read cost more gas than a memory read. State variables that do not change during a loop can be stored in local variables and be read from memory multiple times to save gas.
Referenced code: LibPool.sol#L89 LibSherX.sol#L60 LibSherX.sol#L94 PoolBase.sol#L131 SherX.sol#L76 SherX.sol#L98 SherX.sol#L152 SherX.sol#L184 SherX.sol#L243 Gov.sol#L190
For example, consider re-writing the harvestFor(address)
function of SherX
as follows:
function harvestFor(address _user) public override { GovStorage.Base storage gs = GovStorage.gs(); uint256 len = gs.tokensStaker.length; for (uint256 i; i < len; i++) { PoolStorage.Base storage ps = PoolStorage.ps(gs.tokensStaker[i]); harvestFor(_user, ps.lockToken); } }
#0 - Evert0x
2021-07-31T08:48:44Z
Interesting, my assumption was that loops were copying the .length variable in memory automatically.
shw
At line 185 of Payout
, the deduction
is calculated by excludeUsd / (curTotalUsdPool / totalSupply)
instead of excludeUsd * totalSupply / curTotalUsdPool
. However, the former one is consider less precise than the latter, and could cause a divide-by-zero error if curTotalUsdPool < totalSupply
.
Referenced code: Payout.sol#L185
Change the calculation to excludeUsd * totalSupply / curTotalUsdPool
.
#0 - Evert0x
2021-07-31T08:14:43Z
#24
🌟 Selected for report: shw
229.9835 USDC - $229.98
shw
The _accrueSherX
function of LibSherX
and the payOffDebtAll
function of LibPool
can be called multiple times in the same block (from different users and transactions). If the current block number is the same as the last-recorded one, it is possible to save gas by early returning at the beginning of the functions.
Referenced code: LibSherX.sol#L123-L141 LibPool.sol#L84-L95
For example, consider re-writing _accrueSherX
as follows:
function _accrueSherX(IERC20 _token, uint256 sherXPerBlock) private returns (uint256 sherX) { PoolStorage.Base storage ps = PoolStorage.ps(_token); if (block.number == ps.sherXLastAccrued) { return 0; } sherX = block.number.sub(ps.sherXLastAccrued).mul(sherXPerBlock).mul(ps.sherXWeight).div( uint16(-1) ); // need to settle before return, as updating the sherxperlblock/weight // after it was 0 will result in a too big amount (accured will be < block.number) ps.sherXLastAccrued = uint40(block.number); if (address(_token) == address(this)) { ps.stakeBalance = ps.stakeBalance.add(sherX); } else { ps.unallocatedSherX = ps.unallocatedSherX.add(sherX); ps.sWeight = ps.sWeight.add(sherX); } }
#0 - Evert0x
2021-07-31T09:00:54Z
uint256 last = ps.sherXLastAccrued
to only read from storage once