Platform: Code4rena
Start Date: 21/06/2022
Pot Size: $55,000 USDC
Total HM: 29
Participants: 88
Period: 5 days
Judge: gzeon
Total Solo HM: 7
Id: 134
League: ETH
Rank: 17/88
Findings: 4
Award: $845.58
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: kenzo
Also found by: IllIllI, bardamu, csanuragjain
689.3003 USDC - $689.30
https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L167-L183
As stated in the documentation:
The admin is given privilages to pause specific external principal token wrapping. This may be important in the event of external protocol insolvency or bugs. In addition, the admin is able to able to withdraw fees.
However, minting functionality in the Lender
contract is impossible to pause. This would allow users to keep minting tokens corresponding to principals that have been paused due to insolvency or other issues.
Lender methods signal that they can be paused with the unpaused(p)
modifier (see https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L750-L755).
/// @notice reverts on all markets where the paused mapping returns true /// @param p principal enum value modifier unpaused(uint8 p) { if (paused[p]) { revert Invalid('paused'); } _; }
This modifier is applied to every lend
method in the contract:
However, the mint
function (https://github.com/code-423n4/2022-06-illuminate/blob/main/lender/Lender.sol#L167-L183) does not enable the pausing feature and would allow users to continue minting Illuminate tokens for paused principals.
vim
Add unpaused(p)
modifier to the mint
function so the administrator is able to pause it in case of protocol insolvency or bugs.
#0 - KenzoAgada
2022-06-28T06:34:47Z
Duplicate of #260 (where I detailed the implications)
#1 - sourabhmarathe
2022-06-30T19:09:41Z
Given the implications laid out in #260, this may be upgraded to High Risk as user funds are implicitly diluted by an attacker taking advantage of an insolvent protocol.
29.8781 USDC - $29.88
An attacker would be able to mint an arbitrary large amount of Illuminate tokens regardless of the provided underlying amount.
One of the parameters passed to Sense's lend method is the AMM to be used for swapping. This parameter is constructed by the caller and can therefore point to any address, including an attacker-controlled contract.
[...] /// @param x amm that is used to conduct the swap [...] function lend( uint8 p, address u, uint256 m, uint128 a, uint256 r, address x, address s ) public unpaused(p) returns (uint256) {
This method performs some fee calculations and then transfers the user specified amount of funds to the contract.
// Transfer funds from user to Illuminate Safe.transferFrom(IERC20(u), msg.sender, address(this), a);
Next step is calling the swapUnderlyingForPTs
function on the provided Sense's AMM and storing the return value in the returned
variable.
// Swap those tokens for the principal tokens uint256 returned = ISense(x).swapUnderlyingForPTs(s, m, lent, r);
Because x
points to an arbitrary contract and not necessarily a legitimate Sense AMM, returned
is completely controlled by the attacker and can be any arbitrary amount.
Finally, the contract mints returned
Illuminate tokens to the caller, regardless of the original transferred amount a
and relying on whatever returned
quantity was returned from the external call to the attacker contract. This effectively allows the attacker to mint any desired amount of tokens.
// Get the address of the ERC5095 token for this market IERC5095 illuminateToken = IERC5095(principalToken(u, m)); // Mint the illuminate tokens based on the returned amount illuminateToken.mint(msg.sender, returned);
vim
Ensure initial transfer of funds matches minted tokens if possible, or even better, do not allow an arbitrary AMM address to indicate where to perform the swap.
#0 - sourabhmarathe
2022-07-01T18:30:59Z
Duplicate of #349.
🌟 Selected for report: defsec
Also found by: 0x1f8b, 0x29A, 0xDjango, 0xNazgul, 0xNineDec, 0xf15ers, 0xkowloon, 0xmint, Bnke0x0, BowTiedWardens, Chom, ElKu, Funen, GalloDaSballo, GimelSec, IllIllI, JC, Kenshin, Kulk0, Lambda, Limbooo, MadWookie, Metatron, Picodes, Soosh, StErMi, TomJ, WatchPug, Waze, Yiko, _Adam, ak1, asutorufos, aysha, bardamu, catchup, datapunk, delfin454000, dipp, fatherOfBlocks, grGred, hake, hansfriese, hyh, joestakey, kebabsec, kenzo, kirk-baird, oyc_109, pashov, poirots, rfa, robee, saian, sashik_eth, shenwilly, simon135, slywaters, z3s, zeesaw, zer0dot
63.9425 USDC - $63.94
In order to ensure proper traceability it is recommended to emit events when critical variables are updated. The following functions do not emit any events to signal value changes:
MarketPlace's sellPrincipalToken
seems to use market value without the possibility of specifying a minimum expected amount out.
Note to reviewer: I think this one could maybe represent a MEDIUM level risk.
Lender.sol
contains approve
methods for single and bulk approvals. However, no way to reset these approvals is available. This means that in the case of a buggy approved redeemer or a wrong address being approved, there would be no way to revoke the approval, potentially putting funds in danger until the lender can be redeployed and funds moved to a new contract.
Note to reviewer: I think this one could maybe represent a MEDIUM level risk.
Several functions in Lender.sol and Redeemer.sol perform external calls to user-provided contracts, therefore allowing reentrancy. While I did not manage to find a clear exploitation path, restricting reentrancy to lend()
and redeem()
methods would be recommended to err on the safe side.
ISense(d).redeem(o, m, amount);
where d
is user-suppliedpurchased = IElement(e).swap(swap, fund, r, d);
where e
is user-supplied} else if (ISense(s).pt() != address(token)) {
where s
is user-supplied} else if (ISense(x).maturity() > m)
where x
is user-supplieduint256 returned = IAPWineRouter(pool).swapExactAmountIn(i, 1, lent, 0, r, address(this));
where pool
is user-suppliedIAave(aave).deposit(u, lent, address(this), 0);
where aave
is user-supplieduint128 returned = IYield(y).sellBasePreview(Cast.u128(a));
where y
is user-supplied (see here and here for non-internal function usage)Single letter variable names are used throughout the contracts. While these are in general not hard to figure out, using a more descriptive naming convention could help code readability.
🌟 Selected for report: BowTiedWardens
Also found by: 0v3rf10w, 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, 0xkowloon, Bnke0x0, ElKu, Fitraldys, Funen, GalloDaSballo, IllIllI, JC, Kaiziron, Lambda, MadWookie, Noah3o6, Nyamcil, RoiEvenHaim, TomJ, Tomio, UnusualTurtle, Waze, _Adam, ajtra, asutorufos, bardamu, c3phas, catchup, datapunk, defsec, delfin454000, fatherOfBlocks, grGred, hake, hansfriese, hyh, ignacio, joestakey, kebabsec, ladboy233, oyc_109, pashov, poirots, rfa, robee, sach1r0, samruna, sashik_eth, simon135, slywaters, z3s, zer0dot
62.4602 USDC - $62.46
!= 0
costs 6 less GAS compared to > 0
for unsigned integers in conditional statements with the optimizer enabled.
!= 0
instead of > 0
in return feenominator > 0 ? a / feenominator : 0;
++i
costs less gas compared to i++
or i += 1
for unsigned integers. This is because the pre-increment operation is cheaper (about 5 GAS per iteration).
++i
in single approve
++i
in bulk approve
++i
in Swivel lender
In the EVM, there is no opcode for non-strict inequalities (>=
, <=
) and two operations are performed (>
+ =
.) Consider replacing >=
and <=
with their strict counterparts >
and <
.