Platform: Code4rena
Start Date: 28/06/2022
Pot Size: $25,000 USDC
Total HM: 14
Participants: 50
Period: 4 days
Judge: GalloDaSballo
Total Solo HM: 7
Id: 141
League: ETH
Rank: 1/50
Findings: 5
Award: $3,313.85
🌟 Selected for report: 3
🚀 Solo Findings: 2
🌟 Selected for report: Picodes
2386.7697 USDC - $2,386.77
https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/ERC20.sol#L33 https://github.com/Plex-Engineer/lending-market/blob/ab31a612be354e252d72faead63d86b844172761/contracts/ERC20.sol#L95
_totalSupply
can be initialized to something different than 0, which would lead to an inaccurate total supply, and could easily break integrations, computations of market cap, etc.
If the constructor is called with _initialSupply = 1000, the totalSupply
will be initialized to 1000.
As all the others computations are correct, there will be for ever a discrepancy of 1000 between the real total supply and the one of the contract.
Remove _initialSupply
.
#0 - GalloDaSballo
2022-08-14T23:35:51Z
Same bug as from Canto V1. Recommend the sponsor to just set to 0 and remove the assignment from the constructor
#1 - GalloDaSballo
2022-08-18T19:39:39Z
#2 - Shungy
2022-09-03T02:02:21Z
In the provided contracts, v2 repo is included: https://github.com/code-423n4/2022-06-canto-v2
However, in this submission, the second line of code provided links to the v1 repo. The described issue only exists in v1 version. In v2 version the issue does not exist because msg.sender balance is updated along with the total supply: https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/ERC20.sol#L34
Therefore this finding seems invalid.
#3 - GalloDaSballo
2022-09-03T14:36:59Z
In the provided contracts, v2 repo is included: https://github.com/code-423n4/2022-06-canto-v2
However, in this submission, the second line of code provided links to the v1 repo. The described issue only exists in v1 version. In v2 version the issue does not exist because msg.sender balance is updated along with the total supply: https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/ERC20.sol#L34
Therefore this finding seems invalid.
You're right, I must have missed the line with the mitigation.
The current code will update the _totalSupply
and will give the balance to the deployer.
This is a mistake on my part and the finding should have been closed as invalid as it was mitigated in the V2 code in scope.
#4 - GalloDaSballo
2022-09-03T14:41:23Z
While a nitpick I'd recommend changing the code to use _mint
as it the code in scope will not emit an event which may cause issues if you're tracking via theGraph or similar
Either way I made a mistake here, sorry about that
70.4682 USDC - $70.47
The ensure
modifier is commented, so deadlines will not work when passing orders, breaking this functionnality.
#0 - GalloDaSballo
2022-08-13T23:16:27Z
The warden has shown how, due to a comment, the modifier deadline
doesn't work.
Because Front-running is a key aspect of AMM design, deadline
is a useful tool to ensure that your tx cannot be "saved for later"
Due to the removal of the check, it may be more profitable for a miner to deny the transaction from being mined until the transaction incurs the maximum amount of slippage.
The lack of deadline means that the tx can be withheld indefinitely at the advantage of the miner
For those reasons I agree with Medium Severity
🌟 Selected for report: Picodes
716.0309 USDC - $716.03
https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/CNote.sol#L33 https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Note.sol#L14
The fact that there is no cap on the amount of Note that can be borrowed makes the Oracle Extractable Value unlimited. But as you intend to rely on TWAP, you need to make sure the cost of oracle manipulation is lower than the Oracle Extractable Value.
By manipulating the TWAPs of the designated proxy used for Note (USDC ?) and its relative price to a given collateral(which would be highly costly), an attacker could borrow Note without limit, and empty all pools related to Note and all Note-related value, instantly killing the stablecoin.
The value extractable by Oracle Manipulations is usually easily computable as it is the size of the lending market, but here, it’s more difficult to evaluate as it could potentially be any value linked to Note. This makes risk management harder and increase significantly the risk of attack.
Therefore a cap on how many Notes can be borrowed needs to be added to mitigate this risk.
The attack would be:
Essentially as you have no cap on the amount of Note that could be borrowed in such a scenario, you cannot be sure that the potential attack profits are lower than the attack cost.
The governance needs to set a limit on how much Note can be borrowed to mitigate risks, or add for example an “hourly” borrowing limit.
Easiest way to do this would be able to mint / burn from the accountant
#0 - GalloDaSballo
2022-08-16T16:49:26Z
I don't think you can manipulate the price of cNOTE per this code
//set price statically to 1 when the Comptroller is retrieving Price else if (compareStrings(ctoken.symbol(), "cNOTE") && msg.sender == Comptroller) { return 1; // Note price is fixed to 1 }
However, you can manipulate the price of another token against USDC
else { stablePair = (stable == 0) ? false : true; pair = IBaseV1Pair(pairFor(USDC, underlying, stablePair)); //get the pair for the USDC/underlying pool price = pair.quote(underlying, 1, 8); //how much USDC is this token redeemable for }
The attack outlined by the warden would require an imbalance in the price of an asset against the given above code.
It would also require oracle manipulation, which requires no external arbitrage nor intervention It would require some value to be extractable from the system
For those reasons, I think Medium Severity is more appropriate
🌟 Selected for report: zzzitron
Also found by: 0v3rf10w, 0x1f8b, 0x29A, AlleyCat, Bnke0x0, Chom, Funen, JC, Lambda, Limbooo, Meera, Picodes, Sm4rty, TerrierLover, TomJ, __141345__, asutorufos, aysha, c3phas, cccz, defsec, fatherOfBlocks, grGred, hake, ignacio, ladboy233, mrpathfindr, oyc_109, rfa, sach1r0, samruna, slywaters, ynnad
104.8725 USDC - $104.87
When using an upgradeable proxy over an implementation it is important to ensure the underlying implementation is initialised in addition to the proxied contract.
Consider the following example where we have a Proxy
as contractA and a
Implementation
as contractB . As a parameter to the constructor of contractA the data is provided to make
a delegate call to contractB.initialize()
which updates the storage as required in contractA .
Thus, contractB has not been initialized, only contractA has had its state updated. As a result any user is
able to call initialize()
on contractB , giving themselves full permissions over the contract.
There are two reasons why it is not desirable for malicious users to have full control over a contract:
selfdestruct
which would delete the implementation
contract temporarily making the proxy unusable (until it can be updated with a new implementation); This is not the case for your implementations from what I’ve seen (otherwise this would have been a high issue).Ensure that the initializer
modifier is called on all underlying implementations during deployment. This could easily be done by adding in the implementation:
// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {}
Instead of the old-fashioned, Compound way of coding upgradeable contracts (with XXStorageV1 that you then use as a child for XXStorageV2), it’d be cleaner to use the OpenZeppelin way, were the storage is were it belongs and you just add storage gaps to contracts.
This holds for TreasuryDelegator
and AccountantDelegator
.
Reference: https://medium.com/nomic-foundation-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357
This holds for TreasuryDelegator
, AccountantDelegator
and the others Delegators
.
Reference: https://medium.com/nomic-foundation-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357
type(uint256).max
For readability you can use type(uint256).max
instead of defining uint256 MAX_INT = 2**256-1;
According to your comments decimals is intended to be potentially overloaded. Therefore make it virtual.
_initialSupply
is not used anywhere so could be removed.
hardhat.console
https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Accountant/AccountantDelegate.sol#L7 https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/CErc20.sol#L5 https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-core.sol#L4
payable
in fallback
Instead of using a custom require, why not just removing the payable
modifier in the following fallbacks ?
https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Accountant/AccountantDelegator.sol#L123 https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Treasury/TreasuryDelegator.sol#L121
Delegator
contracts, a lot of functions are useless and could just be remove: the fallback is enoughExamples: https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Treasury/TreasuryDelegator.sol#L63 https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Treasury/TreasuryDelegator.sol#L67
#0 - GalloDaSballo
2022-08-16T21:00:43Z
I don't believe this applies to the codebase as the pattern: -> Base Contract with Offset -> DelegateCall -> Implementation Contract with Offset means that the storage is actually kept in BaseContract, and the Offset is used to ensure that the Implementation will not break the BaseContract (which can still happen if done improperly)
However I don't think them implementation needs initialization "in general"
Similar reasoning here, I think Refactoring is more appropriate and No Gaps are necessary for the Implementations as long as they follow the Offset Pattern R
This is a valid finding in that a clashing implementation will never be executable as the BaseContract selector will match
L
R
## [NC - 02] - ERC20: make decimals virtual Nice Catch, valid R
R
NC
R
R
Because of the clash mentioned above, I'm not counting this as valid, I'd assume the idea is to avoid the clashes as mentioned above
NC
#1 - GalloDaSballo
2022-08-16T21:01:10Z
Nice report, a couple copy pasta but rest is pretty unique
#2 - GalloDaSballo
2022-08-16T21:02:04Z
1L 6R 2NC
🌟 Selected for report: 0x1f8b
Also found by: 0x29A, 0xArshia, 0xKitsune, Bnke0x0, Chom, Fitraldys, Funen, JC, Lambda, Meera, Noah3o6, Picodes, RedOneN, Rohan16, Sm4rty, TerrierLover, TomJ, Tomio, Waze, ajtra, c3phas, cRat1st0s, defsec, durianSausage, fatherOfBlocks, grGred, hake, ladboy233, m_Rassska, mrpathfindr, oyc_109, rfa, ynnad
35.707 USDC - $35.71
_initialSupply
is not used anywhere so could be removed.
Instead of using error strings, to reduce deployment and runtime cost, you should use Custom Errors: see https://blog.soliditylang.org/2021/04/21/custom-errors/. This would save both deployment and runtime cost.
Examples: https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-periphery.sol#L104 https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-periphery.sol#L106
Instead of doing https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-periphery.sol#L495 or https://github.com/Plex-Engineer/lending-market-v2/blob/ea5840de72eab58bec837bb51986ac73712fcfde/contracts/Stableswap/BaseV1-periphery.sol#L500, it’d be more efficient to have setters and test that addresses are the same as it would avoid an external call.
#0 - GalloDaSballo
2022-08-14T21:02:56Z
100 gas from the CALL
also another 2.1k here, another 200 for the second CALL
+ SLOAD
.
Rest is negligible
I recommend focusing on immutables to keep it short and get additional savings