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: 22/88
Findings: 5
Award: $602.51
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: Lambda
Also found by: 0x29A, Chom, cryptphi, itsmeSTYJ, kenzo, kirk-baird, sashik_eth
Anyone can redeem without approval of the owner of the token
In this line: require(_allowance[holder][msg.sender] >= underlyingAmount, 'not enough approvals');
, the underlyingAmount
it's always 0, the require never revert and anyone can get the tokens
Review
On L116, I recommend that change underlyingAmount
to principalAmount
require(_allowance[holder][msg.sender] >= principalAmount, 'not enough approvals');
See the Low-risk vulnerability (submited on my QA And low): [L-01] Repeated code See the Medium-risk vulnerability: [M-01] The allowance does not decrease
With all change:
/// @notice At or after maturity, Burns principalAmount from `owner` and sends exactly `underlyingAmount` of underlying tokens to `receiver`. /// @param underlyingAmount The amount of underlying tokens withdrawn /// @param receiver The receiver of the underlying tokens being withdrawn /// @return principalAmount The amount of principal tokens burnt by the withdrawal function withdraw(uint256 underlyingAmount, address receiver, address holder) external override returns (uint256 principalAmount) { return _redeem(underlyingAmount, receiver, holder); } /// @notice At or after maturity, burns exactly `principalAmount` of Principal Tokens from `owner` and sends underlyingAmount of underlying tokens to `receiver`. /// @param receiver The receiver of the underlying tokens being withdrawn /// @return underlyingAmount The amount of underlying tokens distributed by the redemption function redeem(uint256 principalAmount, address receiver, address holder) external override returns (uint256 underlyingAmount) { return _redeem(principalAmount, receiver, holder); } function _redeem(uint256 amount, address receiver, address holder) private returns (uint256) { if (block.timestamp < maturity) { revert Maturity(maturity); } if (holder == msg.sender) { return IRedeemer(redeemer).authRedeem(underlying, maturity, msg.sender, receiver, amount); } else { _decreaseAllowance(holder, amount); return IRedeemer(redeemer).authRedeem(underlying, maturity, holder, receiver, amount); } }
#0 - KenzoAgada
2022-06-28T06:19:08Z
Duplicate of #173
🌟 Selected for report: kenzo
Also found by: 0x29A, GimelSec, Lambda, StErMi, cccz, csanuragjain, kirk-baird, sashik_eth, shenwilly
146.529 USDC - $146.53
When using the withdraw
and redeem
functions the _allowance
, in both, don't decrease
L99-L102 and L115-L118: In these else don't decrease _allowance[holder][msg.sender]
Review
Decrease the _allowance
in both functions with _decreaseAllowance(address src, uint wad)
:
function withdraw(uint256 underlyingAmount, address receiver, address holder) external override returns (uint256 principalAmount){ if (block.timestamp < maturity) { revert Maturity(maturity); } if (holder == msg.sender) { return IRedeemer(redeemer).authRedeem(underlying, maturity, msg.sender, receiver, underlyingAmount); } else { + _decreaseAllowance(holder, underlyingAmount); return IRedeemer(redeemer).authRedeem(underlying, maturity, holder, receiver, underlyingAmount); } }
function redeem(uint256 principalAmount, address receiver, address holder) external override returns (uint256 underlyingAmount){ if (block.timestamp < maturity) { revert Maturity(maturity); } if (holder == msg.sender) { return IRedeemer(redeemer).authRedeem(underlying, maturity, msg.sender, receiver, principalAmount); } else { + _decreaseAllowance(holder, principalAmount); return IRedeemer(redeemer).authRedeem(underlying, maturity, holder, receiver, principalAmount); } }
See the Low-risk vulnerability: [L-01] Repeated code See the High-risk vulnerability: [H-01] Anyone can redeem tokens without approval
#0 - KenzoAgada
2022-06-28T06:28:21Z
Duplicate of #245
🌟 Selected for report: hyh
Also found by: 0x1f8b, 0x29A, Chom, Soosh, cccz, csanuragjain, hansfriese, itsmeSTYJ, kenzo, pashov, shenwilly, unforgiven
Anyone with balance of principal
token can burn the tokens of the controller
and transfer tokens from the lender
In the L120 get the balance of the owner, but in the line L126 burn the get balance but from the controller
and in the L128 transfer from lender to this and not to the user like the comment says // Transfer the original underlying token back to the user
Manual Review
The function does not behave according to the natspec documentation I propose this solution based on what I understand you want to do, you should review what really want to do: L116-L131:
if (enumValue == uint8(MarketPlace.Principals.Illuminate)) { // Get Illuminate's principal token IERC5095 token = IERC5095(principal); // Get the amount of tokens to be redeemed from the sender uint256 amount = token.balanceOf(msg.sender); // Make sure the market has matured if (block.timestamp < token.maturity()) { revert Invalid('not matured'); } // Burn the prinicipal token from Illuminate token.burn(msg.sender, amount); // Transfer the original underlying token back to the user Safe.transferFrom(IERC20(redeemToken), lender, msg.sender, amount); emit Redeem(0, redeemToken, maturity, amount); }
#0 - sourabhmarathe
2022-06-28T20:39:10Z
Duplicate of #387.
🌟 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
85.2275 USDC - $85.23
The lender/solmatelib.sol it not used, remove this file
The require in lender/Cast.sol, L09 dont have error message
In solidity the address
data type the equal to Any
In lender/Interfaces.sol, L08 don't use an interface like address
, use directly address
. This affect the code readability, this used in lender/Element.sol, L21, L22 and lender/Lender.sol, L357, L358, L465 files
This upgrade the code readability and quality, files include:
In lender/Lender.sol, L153: feenominator should be swivel
In ERC5095.sol, L88-L119 The code on the withdraw
and redeem
functions are similar(almost the same), unify this functions in one
See the Medium-risk vulnerability: [M-01] The allowance does not decrease See the High-risk vulnerability: [H-01] Anyone can redeem tokens without approval
Use @openzeppelin or @rari-capital/solmate to clarify code avoid mistake and don't repeat logic, PLEASE
The marketplace/ERC20.sol implementation don't have severals improves of gas and good practices:
name
, symbol
and decimals
variables in the contract, these are initialize in the constructordecimals
can be immutable_transfer
don't check if the src
and dst
are not equal than address(0)
_setAllowance
don't check if the owner
and spender
are not equal than address(0)
_mint
don't check if the dst
are not equal than address(0)
_burn
don't check if the src
are not equal than address(0)
@rari-capital/solmate have a good implementation for marketplace/ERC20Permit.sol inside of the ERC20 implementation
The redeemer/Safe.sol, lender/Safe.sol and marketplace/Safe.sol are the same, leave only one
Can move redeemer/Interfaces.sol, lender/Interfaces.sol and marketplace/Interfaces.sol in a interfaces
folder and separate each interface in a separates files
Open TODOs can point to architecture or programming issues that still need to be resolved.
(u, m);
address illuminateToken = principalToken(u, m);(u, m);
to:
address illuminateToken = principalToken(u, m);
msg.sender
In sender: address(this), should be sender: msg.sender,
to continue the call chain
signatures
and orders
In lender/Lender.sol:L247, the lend
function don't validate the signatures
and orders
, the swivelAddr
should validate this parameters, this enables to manipulate the order.premium
and order.principal
and affect the L307 ISwivel(swivelAddr).initiate(orders, amounts, signatures);
lend
functionNumber of orders submitted should be equal to the number of signatures since you got one signature pero order. To solve it add a require to the function;
require(o.lenght == s.length, "Mismatched length order/signature arrays");
🌟 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
immutable
The apwineAddr
in redeemer/Redeemer.sol, L33 can be immutable
type(uint256).max
instead of 2**256 - 1
In lender/Lender.sol:
Safe.approve
for (uint256 i; i < len; ) { - IERC20 uToken = IERC20(u[i]); - if (address(0) != (address(uToken))) { - Safe.approve(uToken, a[i], max); + if (address(0) != u[i]) { + Safe.approve(IERC20(u[i]), a[i], max); } unchecked { i++; } }