Platform: Code4rena
Start Date: 09/12/2021
Pot Size: $50,000 USDC
Total HM: 19
Participants: 21
Period: 7 days
Judge: 0xean
Total Solo HM: 14
Id: 61
League: ETH
Rank: 12/21
Findings: 2
Award: $1,099.25
π Selected for report: 8
π Solo Findings: 0
π Selected for report: TomFrenchBlockchain
281.6754 USDC - $281.68
TomFrenchBlockchain
Loss of ETH sent to CreditLine
when borrowing.
Borrow accepts ETH which is sent by the lender but doesn't do anything with it resulting in lost funds.
Make this function nonpayable.
#0 - ritik99
2021-12-23T09:44:34Z
The payable
modifier is a remnant of old code - we'll be removing that. The issue mentioned however stems from the end-user not performing basic sanity checks - sending assets to a contract that does not expect it. We would recommend a (0) non-critical rating for this issue
#1 - 0xean
2022-01-21T21:42:19Z
This is a case of function incorrect to spec
1 β Low: Low: Assets are not at risk. State handling, function incorrect as to spec, issues with comments.
π Selected for report: TomFrenchBlockchain
Also found by: pmerkleplant
126.7539 USDC - $126.75
TomFrenchBlockchain
Detailed description of the impact of this finding.
Take for example the depositCollateral
function. It's payable but the pool may not use ETH as collateral
In the case where the user is performing a direct deposit then we'll pull in the erc20 collateral asset using the transferTokens
function however there's no check for msg.value == 0
when transferring an ERC20 asset.
Similarly when depositing ETH from a savings account any ETH in msg.value
will be lost.
Add a check that msg.value is zero on all code paths which do not handle ETH.
Consider whether to simplify to just use WETH.
#0 - ritik99
2021-12-23T08:20:58Z
We'll add extra checks on our end, but the underlying issue would stem from the end-user not performing proper checks - sending ETH during the ERC20 pulls when it isn't required. payable
is necessary because there are instances where ETH could be the collateral
#1 - 0xean
2022-01-21T21:45:42Z
going to leave as low risk, certainly would be a user error, but doesn't mean it doesn't result in a bad outcome that could be mitigated with the checks.
π Selected for report: TomFrenchBlockchain
281.6754 USDC - $281.68
TomFrenchBlockchain
Inability to set a sensible range which covers all borrow assets without being overly wide.
PoolFactory
has a set requirement that created pool must ask to borrow an amount of assets which are within a certain range as encoded in the poolSizeLimit
.
This doesn't take into account the relative values of each borrow asset so it's hard to choose a one-size-fits-all value for these limits (A 100 WBTC loan could be sensible but a 100 USDC loan will be dwarfs by the gas costs to deploy the pool.)
Place limits on the USD value of the borrowed amount as reported by the factory's price oracle.
π Selected for report: TomFrenchBlockchain
83.4956 USDC - $83.50
TomFrenchBlockchain
Gas costs on linking/unlinking addresses
The activation timestamp can be restricted to a uint64
variable so that it shares a slot with the masterAddress
. This will save an SSTORE and a substantial amount of gas.
As above.
π Selected for report: TomFrenchBlockchain
83.4956 USDC - $83.50
TomFrenchBlockchain
Extra gas costs
As we always deposit the full amount which is approved immediately we know that the allowance will be reset to zero. This line which protects against tokens which require resetting the allowance to zero before approving is therefore redundant.
Remove this line and its equivalent in other yield contracts
π Selected for report: TomFrenchBlockchain
83.4956 USDC - $83.50
TomFrenchBlockchain
Gas costs
In Pool.withdrawBorrowedAmount
we set the loan to active and delete the withdrawal deadline (see link).
When checking whether we can liquidate the pool we check that the loan is active and that block.timestamp > poolConstants.loanWithdrawalDeadline
. This second condition always resolves true as poolConstants.loanWithdrawalDeadline = 0
after deletion. We can then save an SLOAD by skipping this second check.
As above.
π Selected for report: 0x1f8b
Also found by: TomFrenchBlockchain
TomFrenchBlockchain
Gas costs
In the PoolConstants
struct we have a number of fields which don't need to take up an entire storage slot. Easy examples are the timestamps which can be placed in a uint64
variable and packed with an address.
This will save gas when performing the initial writes of these structs.
Use smaller types for variables which don't need 256 bits when saving to storage. Consider which values are being written/read together when choosing how to lay out the packed struct.
#0 - ritik99
2021-12-26T17:44:21Z
Duplicate of #14
π Selected for report: TomFrenchBlockchain
Also found by: WatchPug
37.573 USDC - $37.57
TomFrenchBlockchain
gas costs
(msg.sender == creditLineConstants[_id].borrower && _requestByLender) || (msg.sender == creditLineConstants[_id].lender && !_requestByLender)
is equivalent to
_requestByLender ? (msg.sender == creditLineConstants[_id].borrower) : (msg.sender == creditLineConstants[_id].lender)
or
msg.sender == (_requestByLender ? creditLineConstants[_id].borrower : creditLineConstants[_id].lender)
Which avoid loading the borrower address in the case where the borrower made the request.
Use simplified logic
π Selected for report: TomFrenchBlockchain
83.4956 USDC - $83.50
TomFrenchBlockchain
Gas costs
Nested mappings cost N * (30+2*6) = 42N
gas to calculate the storage slot where N is the number of layers of the mapping.
In SavingsAccount
we have two 3 layer mappings which cost 126 gas to calculate the slot for. If we flatten this mapping to use a single id balanceId = keccak(abi.encodePacked(user, token, strategy))
(this is secure as the packed encoding is non-ambiguous) we then just have to pay (30+2*6) + (30+3*6) = 90
gas.
Flatten these mappings and add private functions for calculating the relevant ids
#0 - ritik99
2021-12-23T07:56:14Z
This might aid in reducing costs, but we plan to retain the current implementation for readability