GithubHelp home page GithubHelp logo

2023-05-ironbank-judging's Introduction

Issue H-1: supplyNativeToken will strand ETH in contract if called after ACTION_DEFER_LIQUIDITY_CHECK

Source: #361

Found by

0x52, CMierez, evilakela

Summary

supplyNativeToken deposits msg.value to the WETH contract. This is very problematic if it is called after ACTION_DEFER_LIQUIDITY_CHECK. Since onDeferredLiqudityCheck creates a new context msg.value will be 0 and no ETH will actually be deposited for the user, causing funds to be stranded in the contract.

Vulnerability Detail

TxBuilderExtension.sol#L252-L256

function supplyNativeToken(address user) internal nonReentrant {
    WethInterface(weth).deposit{value: msg.value}();
    IERC20(weth).safeIncreaseAllowance(address(ironBank), msg.value);
    ironBank.supply(address(this), user, weth, msg.value);
}

supplyNativeToken uses the context sensitive msg.value to determine how much ETH to send to convert to WETH. After ACTION_DEFER_LIQUIDITY_CHECK is called, it enters a new context in which msg.value is always 0. We can outline the execution path to see where this happens:

execute > executeInteral > deferLiquidityCheck > ironBank.deferLiquidityCheck > onDeferredLiquidityCheck (new context) > executeInternal > supplyNativeToken

When IronBank makes it's callback to TxBuilderExtension it creates a new context. Since the ETH is not sent along to this new context, msg.value will always be 0. Which will result in no ETH being deposited and the sent ether is left in the contract.

Although these funds can be recovered by the admin, it may can easily cause the user to be unfairly liquidated in the meantime since a (potentially significant) portion of their collateral hasn't been deposited. Additionally in conjunction with my other submission on ownable not being initialized correctly, the funds would be completely unrecoverable due to lack of owner.

Impact

User funds are indefinitely (potentially permanently) stuck in the contract. Users may be unfairly liquidated due to their collateral not depositing.

Code Snippet

TxBuilderExtension.sol#L252-L256

Tool used

Manual Review

Recommendation

msg.value should be cached at the beginning of the function to preserve it across contexts

Discussion

ibsunhub

Confirm the issue!

However, we believe the correct modification is to pass msg.value through the whole external call and make deferLiquidityCheck function payable.

0xffff11

Valid high. I also think the fix from sponsor is most accurate.

IAm0x52

Passing through msg.value will result in it being nonfunctional in the event that supplyNativeToken is called before ACTION_DEFER_LIQUIDITY_CHECK since it will deposit msg.value into WETH. Then when it calls deferLiquidityCheck it would again attempt to send msg.value which would cause it to revert due to lack of funds.

My suggestion would be to cache msg.value as an internal storage variable (i.e. _msgValue) at the beginning of execute. Adjust supplyNativeToken to use that storage variable rather than msg.value. After the supply to ironBank reset this variable to 0. This allows you to preserve the msg.value across all contexts

ibsunhub

The situation mentioned is same with #192. The solution sounds viable and better. Will work on a fix according to the recommendation.

0xffff11

@ibsunhub You mean that #192 should be a dup of this one?

ibsunhub

No, just think they are related.

iamjakethehuman

Escalate for 10 USDC I believe this issue to be of Medium Severity. Here's why:

  1. It is valid for one asset only (ETH)
  2. It requires to call two specific operations and in a specific order in order for the issue to occur
  3. The lost ETH can be rescued by the owner of the protocol (we do not take into consideration there is a vulnerability the eth can be stolen by adversary as the Watson has both not mentioned it, nor reported it separately)
  4. The biggest loss that can occur is getting liquidated. This would be the case if the user has no more assets to still supply their account. And even if liquidation is to occur, in worst case scenario, they'd lose up to just 20% of their portfolio in IronBank (max liquidation bonus = 125%, (125-100)/125 = 20%. With all these external factors, considering in many cases there would be no loss of funds whatsoever and just in a tiny bit of them there would be a loss of 20%, I believe this to be of Medium severity.

sherlock-admin

Escalate for 10 USDC I believe this issue to be of Medium Severity. Here's why:

  1. It is valid for one asset only (ETH)
  2. It requires to call two specific operations and in a specific order in order for the issue to occur
  3. The lost ETH can be rescued by the owner of the protocol (we do not take into consideration there is a vulnerability the eth can be stolen by adversary as the Watson has both not mentioned it, nor reported it separately)
  4. The biggest loss that can occur is getting liquidated. This would be the case if the user has no more assets to still supply their account. And even if liquidation is to occur, in worst case scenario, they'd lose up to just 20% of their portfolio in IronBank (max liquidation bonus = 125%, (125-100)/125 = 20%. With all these external factors, considering in many cases there would be no loss of funds whatsoever and just in a tiny bit of them there would be a loss of 20%, I believe this to be of Medium severity.

You've created a valid escalation for 10 USDC!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

ibsunhub

The situation mentioned is same with #192. The solution sounds viable and better. Will work on a fix according to the recommendation.

Sorry, I mean #198 related to this issue, not #192 .

C-Mierez

My two cents on iamjakethehuman 's escalation.

  1. It requires to call two specific operations and in a specific order in order for the issue to occur

I would argue that this set of actions is not "too" specific. Deferring liquidity is done to avoid wasting gas by executing IronBank#_getAccountLiquidity() multiple times, so you always want to call this at the beginning before performing all other actions (This behaviour/order can even be seen in TestTxBuilderExtension_integration.t.sol as well). Thus having ACTION_DEFER_LIQUIDITY_CHECK before any of the problematic ACTION_X_NATIVE_TOKEN actions is not far fetched at all, imo.

  1. The lost ETH can be rescued by the owner of the protocol (we do not take into consideration there is a vulnerability the eth can be stolen by adversary as the Watson has both not mentioned it, nor reported it separately)

I explored this possibility in my own report (#368 ), and I don't think we should just ignore the fact that the ETH stuck in the contract can be stolen. If the ETH were safe then this would just be an inconvenience until the owner comes in, but both facts together create a vector in which the user can lose their funds without virtually any cost or risk on the malicious actor's side, all due to this implementation flaw on TxBuilderExtender

ib-tycho

ibdotxyz/ib-v2#53

0xffff11

Thanks for both comments. Imo this issue should be high. I agree, that it is quite likely for it to happen.

''' Deferring liquidity is done to avoid wasting gas by executing IronBank#_getAccountLiquidity() multiple times, so you always want to call this at the beginning before performing all other actions (This behaviour/order can even be seen in TestTxBuilderExtension_integration.t.sol as well). ''' Sponsor has also confirmed. This being said, to fully tie together the eth reports, the issue #198 should now be valid imo as a medium. Because eth can in fact get stuck in the contract. So I would say, for this issue, keep it as a high and upgrade #198 to medium

ibsunhub

fix: ibdotxyz/ib-v2#53

hrishibhat

Result: High Has duplicates Affecting only one token can still be a valid high, the given order of operations is not an unlikely scenario. And since this break the assumption of these contracts should not hold eth + it can be stolen as shown in #368, Maintaining the severity as is.

sherlock-admin

Escalations have been resolved successfully!

Escalation status:

IAm0x52

Fix looks good. Msg.value is now cached allowing it to be preserved across contexts

Issue M-1: PriceOracle.getPrice doesn't check for stale price

Source: #9

Found by

0x3b, 0x52, 0x8chars, 0xHati, 0xStalin, 0xpinky, 3agle, Angry_Mustache_Man, Arabadzhiev, Aymen0909, Bauchibred, BenRai, Bozho, Breeje, Brenzee, BugBusters, CMierez, Delvir0, Diana, Hama, HexHackers, IceBear, Ignite, Jaraxxus, Kodyvim, Kose, Madalad, MohammedRizwan, Norah, Ocean_Sky, Pheonix, Proxy, R-Nemes, Ruhum, Schpiel, SovaSlava, anthony, berlin-101, bin2chen, bitsurfer, branch_indigo, devScrooge, evilakela, gkrastenov, harisnabeel, holyhansss, jayphbee, josephdara, kn0t, kutugu, lil.eth, martin, n33k, ni8mare, plainshift-2, qbs, qpzm, rvierdiiev, saidam017, santipu_, sashik_eth, shaka, shealtielanz, simon135, sl1, thekmj, toshii, tsvetanovv, vagrant

Summary

PriceOracle.getPrice doesn't check for stale price. As result protocol can make decisions based on not up to date prices, which can cause loses.

Vulnerability Detail

PriceOracle.getPrice function is going to provide asset price using chain link price feeds. https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

    function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

This function doesn't check that prices are up to date. Because of that it's possible that price is not outdated which can cause financial loses for protocol.

Impact

Protocol can face bad debt.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

You need to check that price is not outdated by checking round timestamp.

Discussion

ibsunhub

Same with the answer to #25. It's not practical to setup different heartbeat for individual markets. And we have a backend to monitor the price deviation.

0xffff11

Due to the off-chain system by Iron, issue can be a low. (Does not really affect the current state of the contracts) @ibsunhub Is it the right resolution, or thinking more about invalid?

ib-tycho

We still think this is invalid, thanks

0xffff11

Because Iron's off-chain safeguard, invalid

bzpassersby

Escalate for 10 USDC I think this is wrongly classified as invalid. (1) It's impossible for Watsons to know that the protocol has off-chain safeguards because the protocol explicitly said there are no off-chain mechanisms in the contest info. It's unfair for Watsons who might be misled by this answer.

Q: Are there any off-chain mechanisms or off-chain procedures for the protocol (keeper bots, input validation expectations, etc)?
nope

(2)It's right and should be encouraged for Watsons to point-out insufficient on-chain checks. The current code ignores any data freshness-related variables when consuming chainlink data, which is clearly not the best practice.

And it's understandable that the protocol chose to implement such checks off-chain. But since Watsons wouldn't have known about this and that the code itself clearly has flaws. This should be at least low/informational. It's unfair for Watsons to be punished because of this.

sherlock-admin

Escalate for 10 USDC I think this is wrongly classified as invalid. (1) It's impossible for Watsons to know that the protocol has off-chain safeguards because the protocol explicitly said there are no off-chain mechanisms in the contest info. It's unfair for Watsons who might be misled by this answer.

Q: Are there any off-chain mechanisms or off-chain procedures for the protocol (keeper bots, input validation expectations, etc)?
nope

(2)It's right and should be encouraged for Watsons to point-out insufficient on-chain checks. The current code ignores any data freshness-related variables when consuming chainlink data, which is clearly not the best practice.

And it's understandable that the protocol chose to implement such checks off-chain. But since Watsons wouldn't have known about this and that the code itself clearly has flaws. This should be at least low/informational. It's unfair for Watsons to be punished because of this.

You've created a valid escalation for 10 USDC!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

hrishibhat

Result: Medium Has Duplicates Considering this a valid medium

sherlock-admin

Escalations have been resolved successfully!

Escalation status:

Josephdara

Hi @hrishibhat @sherlock-admin I believe my issue has been omitted #471 (comment)

jacksanford1

Issue was labeled "Won't Fix" by protocol team. Categorizing as an acknowledged issue.

Issue M-2: PriceOracle will use the wrong price if the Chainlink registry returns price outside min/max range

Source: #25

Found by

0x52, 0x8chars, Angry_Mustache_Man, Bauchibred, BenRai, BugBusters, Jaraxxus, Madalad, R-Nemes, bitsurfer, branch_indigo, deadrxsezzz, shaka, thekmj, tsvetanovv

Summary

Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened to Venus on BSC when LUNA imploded.

Vulnerability Detail

Note there is only a check for price to be non-negative, and not within an acceptable range.

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
    (, int256 price,,,) = registry.latestRoundData(base, quote);
    require(price > 0, "invalid price");

    // Extend the decimals to 1e18.
    return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
}

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

A similar issue is seen here.

Impact

The wrong price may be returned in the event of a market crash. An adversary will then be able to borrow against the wrong price and incur bad debt to the protocol.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

Implement the proper check for each asset. It must revert in the case of bad price.

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
    (, int256 price,,,) = registry.latestRoundData(base, quote);
    require(price >= minPrice && price <= maxPrice, "invalid price"); // @audit use the proper minPrice and maxPrice for each asset

    // Extend the decimals to 1e18.
    return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
}

Discussion

ibsunhub

It's not practical to setup the min price and max price for individual asset. It's hard to define a reasonable range for each asset and it will make oracle configuration more complex. It's much easier to make human error.

Also, we had an off-chain backend system to monitor the price from ChainLink. If the price is off, we would intervene to pause the oracle.

0xffff11

@ibsunhub If the oracle is paused, wouldn't functions that require of that oracle response also be paused during that time?

ibsunhub

Yes, functions that need to retrieve the price will revert. They are borrow, redeem, transferIBToken, and liquidate.

0xffff11

So, I see what the watson points out. I see that you have an off-chain safeguard for this. Therefore, I would mark the issue as invalid. Though I don't think the solution should be to revert. Liquidations can be key while oracle is paused. I think the fix should be the one from #433 (secondary oracle and a try catch)

0xffff11

Invalid, Iron has an off-chain safeguard for price deviation that would prevent this

iamjakethehuman

Escalate for 10 USDC The off-chain safeguard is never mentioned. Watsons are not supposed to know it exists. Also, the supposed solution imposes an even larger risk as any user would be able to enter tbe market of which the oracle reverts and avoid liquidations. Issue should be marked as valid and another solution should be proposed.

sherlock-admin

Escalate for 10 USDC The off-chain safeguard is never mentioned. Watsons are not supposed to know it exists. Also, the supposed solution imposes an even larger risk as any user would be able to enter tbe market of which the oracle reverts and avoid liquidations. Issue should be marked as valid and another solution should be proposed.

You've created a valid escalation for 10 USDC!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

ADK0010

Also the contest page doesn't talk about any off-chain safeguards.

ironbank_escalation

hrishibhat

Result: Medium Has duplicates This is a valid medium

sherlock-admin

Escalations have been resolved successfully!

Escalation status:

ib-tycho

How do you establish a reasonable minimum and maximum price range for each asset? The incident related to Venus that you mentioned was caused by the inherent risk of the LUNA token itself. Evaluating the risk associated with an asset should always be taken into account when listing it. I disagree with relying solely on manual human input for setting the price range, as it does not address the underlying issue faced by Venus. Therefore, we will not make changes to address this matter.

jacksanford1

Issue was labeled "Won't Fix" by protocol team. Categorizing as an acknowledged issue.

Issue M-3: Price Oracle contract does not work in Arbitrum and Optimism

Source: #191

Found by

ast3ros, n1punp, sashik_eth, sufi, tnquanghuy0512

Summary

The PriceOracle contract relies on the Chainlink Oracle FeedRegistry to get the price of tokens. However, the FeedRegistry is not available in L2 networks such as Arbitrum and Optimism. This means that the PriceOracle contract will fail to return the price of tokens in these networks.

Vulnerability Detail

The PriceOracle contract uses the Chainlink Oracle FeedRegistry to get the latest round data for a pair of tokens.

    (, int256 price,,,) = registry.latestRoundData(base, quote);

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L67

The Iron Bank is deployed in mainnet, Arbitrum and Optimism. However, according to the Chainlink documentation, the FeedRegistry is only available in mainnet and not in Arbitrum and Optimism.

https://docs.chain.link/data-feeds/feed-registry#contract-addresses

This means that the PriceOracle contract will not be able to get the price of tokens in these L2 networks. This will affect the functionalities of the protocol that depend on the token price, such as liquidation.

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L827-L828

Impact

The PriceOracle contract will not function properly in L2 networks. This will break the protocol functions that rely on the token price.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L67 https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L827-L828

Tool used

Manual Review

Recommendation

Reimplement the PriceOracle contract by reading the price feed from AggregatorV3Interface instead of FeedRegistry. Example: https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Discussion

thangtranth

Escalate for 10 USDC

Please help me to review. This is not a duplication of #440.

  • #440 is about checking If Arbitrum sequencer is down.
  • This issue is about the current PriceOracle contract is wrongly implemented because the FeedRegistry is not existed in Arbitrum and Optimism.

They are two different issues with two different root causes.

sherlock-admin

Escalate for 10 USDC

Please help me to review. This is not a duplication of #440.

  • #440 is about checking If Arbitrum sequencer is down.
  • This issue is about the current PriceOracle contract is wrongly implemented because the FeedRegistry is not existed in Arbitrum and Optimism.

They are two different issues with two different root causes.

You've created a valid escalation for 10 USDC!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

ib-tycho

Regarding the mistake in the contest details mentioned in the README, we apologize for any confusion caused. When we stated that we would deploy on Arbitrum and Optimism, we meant that we would make the necessary modifications before deployment. This is our standard practice of maintaining contracts with different branches, same as what we did in v1: https://github.com/ibdotxyz/compound-protocol/branches

We are aware of the absence of a registry on OP and Arb, as pointed out by some individuals. We would like to inquire if it is possible to offer the minimum reward for an oracle issue on L2. Thank you.

0xffff11

Even though the issue is valid, there will be no deployment on L2s as stated by sponsor. Unfortunately, it is invalid due to a miss-understanding from the docs. As watson said, it is not a duplicate of #440 so it should be de-duplicated and closed.

thangtranth

Hi everyone,

I totally understand the reasoning and I donโ€™t mean to push this issue. However, Iโ€™m concerned that it will set a precedent when Watsons do not know what the scope is and where the source of truth is for each contest.

There are some duplication of this issue such as #18 and #239 which I believe those Watsons also pay attention to every line of codes and check Oracle registries in each L2 to ensure that the code is safely deployed in stated L2 in Readme.

In my opinion, if this issue is invalid, then all other issues related to L2 such as Sequencer should also be marked as invalid since the code is not used in L2.

hrishibhat

Result: Medium Has duplicates Although there was an error in the Readme, this would have been easily handled during deployment. However, this is still a valid issue from the contest POV.

sherlock-admin2

Escalations have been resolved successfully!

Escalation status:

jacksanford1

Issue was labeled "Won't Fix" by protocol team. Categorizing as an acknowledged issue.

Issue M-4: Wrong Price will be Returned When Asset is PToken for WstETH

Source: #220

Found by

branch_indigo

Summary

Iron Bank allows a PToken market to be created for an underlying asset in addition to a lending market. PTokens can be counted as user collaterals and their price is fetched based on their underlying tokens. However, wrong price will return when PToken's underlying asset is WstETH.

Vulnerability Detail

Retrieving price for WstETH is a 2 step process. WstETH needs to be converted to stETH first, then converted to ETH/USD. This is properly implemented when the market is WstETH through checking if (asset==wsteth). But when PToken market is created for WstETH, this check will by bypassed because PToken contract address will be different from wsteth address.

PToken market price is set through _setAggregators() in PriceOracle.sol where base and quote token address are set and tested before writing into aggregators array. And note that quote token address can either be ETH or USD. When asset price is accessed through getPrice(), if the input asset is not wsteth address, aggregators is directly pulled to get chainlink price denominated in ETH or USD.

//PriceOracle.sol
//_setAggregators()
                require(
                    aggrs[i].quote == Denominations.ETH ||
                        aggrs[i].quote == Denominations.USD,
                    "unsupported quote"
                );
//PriceOracle.sol
    function getPrice(address asset) external view returns (uint256) {
        if (asset == wsteth) {
            uint256 stEthPrice = getPriceFromChainlink(
                steth,
                Denominations.USD
            );
            uint256 stEthPerToken = WstEthInterface(wsteth).stEthPerToken();
            uint256 wstEthPrice = (stEthPrice * stEthPerToken) / 1e18;
            return getNormalizedPrice(wstEthPrice, asset);
        }
        AggregatorInfo memory aggregatorInfo = aggregators[asset];
        uint256 price = getPriceFromChainlink(
            aggregatorInfo.base,
            aggregatorInfo.quote
        );
       ...

This creates a problem for PToken for WstETH, because if (asset==wsteth) will be bypassed and chainlink aggregator price will be returned. And chainlink doesn't have a direct price quote of WstETH/ETH or WstETH/USD, only WstETH/stETH or stETH/USD. This means most likely aggregator price for stETH/USD will be returned as price for WstETH.

Since stETH is a rebasing token, and WstETH:stETH is not 1 to 1, this will create a wrong valuation for users holding PToken for WstETH as collaterals.

Impact

Since users holding PToken for WstETH will have wrong valuation, this potentially creates opportunities for malicious over-borrowing or unfair liquidations, putting the protocol at risk.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L43

Tool used

Manual Review

Recommendation

In getPrice(), consider adding another check whether the asset is PToken and its underlying asset is WstETH. If true, use the same bypass for pricing.

Discussion

bzpassersby

Escalate for 10 USDC I think this is wrongly excluded. The report describes a clear vulnerability that the current Oracle implementation doesn't take into account PToken for WstETH. stETH is a rebasing token ,which is correctly factored in oracle implementation when the market asset is for WstETH. However, when a PToken is created for WstETH, if(asset==wsteth) will be bypassed. Since chainlink doesn't have a direct feed for WstETH/ETH or WstETH/USD. Wrong price will be returned.

sherlock-admin

Escalate for 10 USDC I think this is wrongly excluded. The report describes a clear vulnerability that the current Oracle implementation doesn't take into account PToken for WstETH. stETH is a rebasing token ,which is correctly factored in oracle implementation when the market asset is for WstETH. However, when a PToken is created for WstETH, if(asset==wsteth) will be bypassed. Since chainlink doesn't have a direct feed for WstETH/ETH or WstETH/USD. Wrong price will be returned.

You've created a valid escalation for 10 USDC!

To remove the escalation from consideration: Delete your comment.

You may delete or edit your escalation comment anytime before the 48-hour escalation window closes. After that, the escalation becomes final.

0xffff11

Issue seems valid to me. I would judge it as a med because seems that it is for a only a specific pair, quite an edgecases. Thoughts @ibsunhub ?

ibsunhub

We're ok with med.

Although we don't decide that if we will support wstETH's PToken, it's our oversight to handle of the price of wstETH's PToken in the price oracle.

ibsunhub

fix: ibdotxyz/ib-v2#57

hrishibhat

Result: Medium Unique Considering this issue a valid medium based on the above comments.

sherlock-admin

Escalations have been resolved successfully!

Escalation status:

IAm0x52

Fix looks good. PTokens now always use their underlying token when determining their price

Issue M-5: getPriceFromChainlink() doesn't check If Arbitrum sequencer is down in Chainlink feeds

Source: #440

Found by

0x52, 0xMAKEOUTHILL, Angry_Mustache_Man, Arabadzhiev, Arz, Aymen0909, BenRai, Breeje, Brenzee, BugBusters, Delvir0, HexHackers, Ignite, Jaraxxus, Kodyvim, Madalad, MohammedRizwan, Ocean_Sky, Proxy, R-Nemes, SovaSlava, berlin-101, bin2chen, bitsurfer, branch_indigo, deadrxsezzz, devScrooge, josephdara, kutugu, n1punp, n33k, ni8mare, p0wd3r, plainshift-2, rvierdiiev, santipu_, sashik_eth, shaka, simon135, sl1, toshii, tsvetanovv, turvec, vagrant

Summary

When utilizing Chainlink in L2 chains like Arbitrum, it's important to ensure that the prices provided are not falsely perceived as fresh, even when the sequencer is down. This vulnerability could potentially be exploited by malicious actors to gain an unfair advantage.

Vulnerability Detail

There is no check: getPriceFromChainlink

    function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
@>      (, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

Impact

could potentially be exploited by malicious actors to gain an unfair advantage.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

code example of Chainlink: https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Discussion

0xffff11

Valid medium

ib-tycho

Regarding the mistake in the contest details mentioned in the README, we apologize for any confusion caused. When we stated that we would deploy on Arbitrum and Optimism, we meant that we would make the necessary modifications before deployment. This is our standard practice of maintaining contracts with different branches, same as what we did in v1: https://github.com/ibdotxyz/compound-protocol/branches

We are aware of the absence of a registry on OP and Arb, as pointed out by some individuals. We would like to inquire if it is possible to offer the minimum reward for an oracle issue on L2. Thank you.

ib-tycho

We'll fix this when deploying on L2, but we disagree with Severity. I would consider this as Low

0xffff11

According to past reports and sponsor confirmed that they will fix the issue. The issue will remain as a medium.

MLON33

Assuming this issue is acknowledged by the protocol team and wonโ€™t be fixed.

2023-05-ironbank-judging's People

Contributors

evert0x avatar rcstanciu avatar sherlock-admin avatar

Stargazers

 avatar  avatar

Watchers

 avatar

2023-05-ironbank-judging's Issues

sl1 - Medim severity - No check for stale price feeds

sl1

medium

Medim severity - No check for stale price feeds

Summary

getPriceFromChainlink function in PriceOracle.sol does not check for price feed staleness which can lead to wrong price return value.

Vulnerability Detail

Oracle data feed is insufficiently validated. There is no check for stale price. Price can be stale and lead to wrong return value.

Impact

Stale data can lead to wrong price.

Code Snippet

(, int256 price,,,) = registry.latestRoundData(base, quote);
 require(price > 0, "invalid price");

https://github.com/sherlock-audit/2023-05-ironbank/blob/9ebf1702b2163b55479624794ab7999392367d2a/ib-v2/src/protocol/oracle/PriceOracle.sol#L67

Tool used

Manual Review

Recommendation

(, int256 price,, uint256 updatedAt, ) = registry.latestRoundData(base, quote);
        if(updatedAt < block.timestamp - 60 * 60 /* 1 hour */) {
            revert("stale price feed");
        } 
        require(price > 0, "invalid price");

The potential solution is to validate that no more than 1 hour has passed from the "updatedAt" timestamp value returned from "latestRoundData()", otherwise the transaction will revert

Duplicate of #9

BugBusters - ChainLink `latestRoundData()` has no check for round completeness

BugBusters

medium

ChainLink latestRoundData() has no check for round completeness

Summary

No check for round completeness could lead to stale prices and wrong price return value, or outdated price. The functions rely on accurate price feed might not work as expected, sometimes can lead to fund loss.

Vulnerability Detail

The PriceOracle getPriceFromChainlink() call out to an oracle with latestRoundData() to get the price of some token.There is no check for round completeness.

According to Chainlink's documentation, this function does not error if no answer has been reached but returns 0 or outdated round data. The external Chainlink oracle, which provides index price information to the system, introduces risk inherent to any dependency on third-party data sources. For example, the oracle could fall behind or otherwise fail to be maintained, resulting in outdated data being fed to the index price calculations. Oracle reliance has historically resulted in crippled on-chain systems, and complications that lead to these outcomes can arise from things as simple as network congestion.

Impact

If there is a problem with chainlink starting a new round and finding consensus on the new value for the oracle (e.g. chainlink nodes abandon the oracle, chain congestion, vulnerability/attacks on the chainlink system) consumers of this contract may continue using outdated stale data (if oracles are unable to submit no new round is started).

This could lead to stale prices and wrong price return value, or outdated price.

As a result, the functions rely on accurate price feed might not work as expected, sometimes can lead to fund loss. The impacts vary and depends on the specific situation like the following:

Incorrect liquidation
some users could be liquidated when they should not
no liquidation is performed when there should be

wrong price feed
causing inappropriate loan being taken, beyond the current collateral factor

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L107-L108

Tool used

Manual Review

Recommendation

Validate data feed for round completeness:

require(answeredInRound >= roundID, "round not complete");

Duplicate of #9

rvierdiiev - IronBank.redeem should round up ibToken amount

rvierdiiev

medium

IronBank.redeem should round up ibToken amount

Summary

IronBank.redeem doesn't round up ibToken amount that should be removed from user's balance.

Vulnerability Detail

IronBank.redeem function allows user to receive some amount of underlying tokens, in exchange of burning its ibToken.
This is how ibToken amount that should be burnt is calculated:
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L421-L427

        if (amount == type(uint256).max) {
            ibTokenAmount = userSupply;
            amount = (ibTokenAmount * _getExchangeRate(m)) / 1e18;
            isRedeemFull = true;
        } else {
            ibTokenAmount = (amount * 1e18) / _getExchangeRate(m);
        }

In case if amount is not unit256.max, then (amount * 1e18) / _getExchangeRate(m) is used to get ibToken amount.
The problem here is that this calculation should round up ibTokenAmount.

Impact

User redeems less amount of ibToken in exchange of amount of underlying tokens.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Round up ibTokenAmount.

        if (amount == type(uint256).max) {
            ibTokenAmount = userSupply;
            amount = (ibTokenAmount * _getExchangeRate(m)) / 1e18;
            isRedeemFull = true;
        } else {
            ibTokenAmount = (amount * 1e18) / _getExchangeRate(m);
            if (ibTokenAmount * _getExchangeRate(m) / 1e18 != amount) ibTokenAmount += 1; 
        }

kutugu - SafeApprove require allowance is zero

kutugu

medium

SafeApprove require allowance is zero

Summary

SafeApprove require allowance is zero, should use forceApprove instead of safeApprove.

Vulnerability Detail

For flashLoan, when repay contract will approve ironBank.

        IERC20(token).safeTransferFrom(address(receiver), address(this), amount);

        uint256 allowance = IERC20(token).allowance(address(this), ironBank);
        if (allowance < amount) {
            IERC20(token).safeApprove(ironBank, type(uint256).max);
        }

        IronBankInterface(ironBank).repay(address(this), address(this), token, amount);

It starts with allowance of 0, which is less than repay amount, so it will approve ironBank type(uint256).max.
Over a long period of flashloan, allowance is gradually reduced until it is eventually less than the repay amount, but greater than 0. Note that flashloan only limits tokens to market ERC20 tokens(any vanilla ERC20s), it does not specify transferFrom won't reduce an allowance of type(uint256).max.
When safeApprove is called again, due to the fact that allowance is not 0, the call will revert, and this token cannot be loaned thereafter, so it can be considered as permanent dos.

    function safeApprove(IERC20 token, address spender, uint256 value) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

Of course since type(uint256).max is a huge number which is fine for most tokens for a long time. But note again that the totalSupply of market tokens can be large, which has no limit.

Impact

Medium. If allowance is reduced to less than repay amount, flashloan this market token is not available.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/flashLoan/FlashLoan.sol#L108

Tool used

Manual Review

Recommendation

use forceApprove instead of safeApprove.

Duplicate of #420

rvierdiiev - Missing checks for whether Arbitrum Sequencer is active

rvierdiiev

medium

Missing checks for whether Arbitrum Sequencer is active

Summary

PriceOracle doesn't check whether the Arbitrum Sequencer is active when using prices from chainlink oracle.

Vulnerability Detail

Iron Bank protocol is going to launch on arbitrum network.
PriceOracle is using chainlink oracle in order to get prices. Chainlink recommends to check if arbitrum sequencer is active in order to get fresh prices. Otherwise stale prices can be fetched.

Impact

PriceOracle can calculate prices incorrectly which can cause bad debt for a protocol.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

Check that sequencer is not down.

Duplicate of #440

tsvetanovv - It is possible for a user to completely redeem an amount of asset from Iron Bank, but not leave the market

tsvetanovv

high

It is possible for a user to completely redeem an amount of asset from Iron Bank, but not leave the market

Summary

It is possible for a user to completely Redeem an amount of asset from Iron Bank, but not leave the market. This situation is happening in redeem() function:

function redeem(address from, address to, address market, uint256 amount)
ย  ย  ย  ย  external
ย  ย  ย  ย  nonReentrant
ย  ย  ย  ย  isAuthorized(from)
ย  ย  {
ย  ย  ย  ย  DataTypes.Market storage m = markets[market];
ย  ย  ย  ย  require(m.config.isListed, "not listed");

ย  ย  ย  ย  _accrueInterest(market, m);

ย  ย  ย  ย  uint256 userSupply = m.userSupplies[from];
ย  ย  ย  ย  uint256 totalCash = m.totalCash;
  
ย  ย  ย  ย  uint256 ibTokenAmount;
ย  ย  ย  ย  bool isRedeemFull;
ย  ย  ย  ย  if (amount == type(uint256).max) { ย 
ย  ย  ย  ย  ย  ย  ibTokenAmount = userSupply;
ย  ย  ย  ย  ย  ย  amount = (ibTokenAmount * _getExchangeRate(m)) / 1e18;
ย  ย  ย  ย  ย  ย  isRedeemFull = true;
ย  ย  ย  ย  } else {
ย  ย  ย  ย  ย  ย  ibTokenAmount = (amount * 1e18) / _getExchangeRate(m);
ย  ย  ย  ย  }
  
ย  ย  ย  ย  require(userSupply >= ibTokenAmount, "insufficient balance");
ย  ย  ย  ย  require(totalCash >= amount, "insufficient cash");
  
ย  ย  ย  ย  // Update storage.
ย  ย  ย  ย  unchecked {
ย  ย  ย  ย  ย  ย  m.userSupplies[from] = userSupply - ibTokenAmount;
ย  ย  ย  ย  ย  ย  m.totalCash = totalCash - amount;
ย  ย  ย  ย  ย  ย  // Underflow not possible: ibTokenAmount <= userSupply <= totalSupply.
ย  ย  ย  ย  ย  ย  m.totalSupply -= ibTokenAmount;
ย  ย  ย  ย  }
  
ย  ย  ย  ย  // Check if need to exit the market.
ย  ย  ย  ย  if (isRedeemFull && _getBorrowBalance(m, from) == 0) {
ย  ย  ย  ย  ย  ย  _exitMarket(market, from);
ย  ย  ย  ย  }  

ย  ย  ย  ย  IBTokenInterface(m.config.ibTokenAddress).burn(from, ibTokenAmount); // Only emits Transfer event.
ย  ย  ย  ย  IERC20(market).safeTransfer(to, amount);

ย  ย  ย  ย  _checkAccountLiquidity(from);

ย  ย  ย  ย  emit Redeem(market, from, to, amount, ibTokenAmount);
ย  ย  }

Vulnerability Detail

Consider the following situation:

  • Alice wants to redeem a full amount of an asset from the Iron Bank and exit the market.
  • To redeem the full amount she needs to put 100 amount of tokens.
  • After Alice does this we go to the following code:
420:ย  ย  ย  ย  bool isRedeemFull;
421:ย  ย  ย  ย  if (amount == type(uint256).max) { ย 
422:ย  ย  ย  ย  ย  ย  ibTokenAmount = userSupply;
423:ย  ย  ย  ย  ย  ย  amount = (ibTokenAmount * _getExchangeRate(m)) / 1e18;
424:ย  ย  ย  ย  ย  ย  isRedeemFull = true;
425:ย  ย  ย  ย  } else {
426:ย  ย  ย  ย  ย  ย  ibTokenAmount = (amount * 1e18) / _getExchangeRate(m);
427:ย  ย  ย  ย  }
  • Alice actually buys all her tokens (100) and wants to get out of the market, but doesn't actually enter the if statement.
  • The condition amount == type(uint256).max checks whether the amount parameter is equal to type(uint256).max, which is the maximum value representable by a uint256 variable, and Alice enters the if statement.
  • Because she does not enter into the if statement, isRedeemFull is never set to true and stays false.
  • Then we go to the following code in which Alice should exit the market but actually does not pass the check because isRedeemFull remains false
440:ย  ย  ย    // Check if need to exit the market.
441:ย  ย  ย  ย  if (isRedeemFull && _getBorrowBalance(m, from) == 0) {
442:ย  ย  ย  ย  ย  ย  _exitMarket(market, from);
443:ย  ย  ย  ย  }

Impact

If the user not entering the _exitMarket() function means that the user's account will still be considered as having an open position in the market. The outstanding borrow balance will remain, and the user will continue to accrue interest on their borrowings.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L406-L451

Tool used

Manual Review

Recommendation

In the if statement, check how many tokens a user can have, not the maximum number. like this if statement is executed and when a user wants to redeem everything isRedeemFull will be set to true.

rvierdiiev - IronBank._getAccountLiquidity should acrrue interests for all user's markets in order to receive correct collateral/debt balance

rvierdiiev

medium

IronBank._getAccountLiquidity should acrrue interests for all user's markets in order to receive correct collateral/debt balance

Summary

IronBank._getAccountLiquidity should acrrue interests fro all user's markets in order to receive correct collateral/debt balance. Because it's not done now, that means that user can be allowed/disallowed to borrow, when he shouldn't/should be able to.

Vulnerability Detail

_getAccountLiquidity function is used by protocol in order to understand how many collateral and debt user has through the all markets.
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L1032-L1058

    function _getAccountLiquidity(address user) internal view returns (uint256, uint256) {
        uint256 collateralValue;
        uint256 debtValue;

        address[] memory userEnteredMarkets = allEnteredMarkets[user];
        for (uint256 i = 0; i < userEnteredMarkets.length; i++) {
            DataTypes.Market storage m = markets[userEnteredMarkets[i]];
            if (!m.config.isListed) {
                continue;
            }

            uint256 supplyBalance = m.userSupplies[user];
            uint256 borrowBalance = _getBorrowBalance(m, user);

            uint256 assetPrice = PriceOracleInterface(priceOracle).getPrice(userEnteredMarkets[i]);
            require(assetPrice > 0, "invalid price");
            uint256 collateralFactor = m.config.collateralFactor;
            if (supplyBalance > 0 && collateralFactor > 0) {
                uint256 exchangeRate = _getExchangeRate(m);
                collateralValue += (supplyBalance * exchangeRate * assetPrice * collateralFactor) / 1e36 / FACTOR_SCALE;
            }
            if (borrowBalance > 0) {
                debtValue += (borrowBalance * assetPrice) / 1e18;
            }
        }
        return (collateralValue, debtValue);
    }

As you can see this function doesn't accrue interests for all user's markets. That means that collateral balance and debt balance could be wrong for that markets, because accruing interests has impact on both exchange ratio and borrow index.

Impact

User's collateral/debt balance is not up to date.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

You need to accrue interests for all markets of user.

BugHunter101 - Flashloan._loan() does not need the fee, it will cause protocol loss fund.

BugHunter101

high

Flashloan._loan() does not need the fee, it will cause protocol loss fund.

Summary

Flashloan._loan() does not need the fee, it will cause protocol loss fund.

Vulnerability Detail

As we can see , Flashloan._loan() does not need the fee, just transfer amount to user, it will cause protocol loss fund.

function _loan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes memory data, address msgSender)
        internal
    {
        IronBankInterface(ironBank).borrow(address(this), address(receiver), token, amount);

        require(receiver.onFlashLoan(msgSender, token, amount, 0, data) == CALLBACK_SUCCESS, "callback failed"); // no fee

        IERC20(token).safeTransferFrom(address(receiver), address(this), amount);//@audit 

        uint256 allowance = IERC20(token).allowance(address(this), ironBank);
        if (allowance < amount) {
            IERC20(token).safeApprove(ironBank, type(uint256).max);
        }

        IronBankInterface(ironBank).repay(address(this), address(this), token, amount);
    }

Impact

it will cause protocol loss fund.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/flashLoan/FlashLoan.sol#L104

Tool used

Manual Review

Recommendation

adding fee such using calculate rate.

p0wd3r - Oracle is not checking for sequencer uptime

p0wd3r

medium

Oracle is not checking for sequencer uptime

Summary

Oracle is not checking for sequencer uptime

Vulnerability Detail

The ArbitrumSequencer can allow tx to be queued while the L2 is offline, these will pass the freshness check but use outdated prices.

Impact

Because of how Arbitrum works, if the L2 goes offline, tx can still be sent via the Delayed inbox on L1.

This could allow the creation of orders which use prices from when the Sequencer was down, which would still pass the freshness check, but may be incorrect or outdated

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66

Tool used

Manual Review

Recommendation

Add the check for the Sequencer being online, see the CL docs for more details:
https://docs.chain.link/data-feeds/l2-sequencer-feeds#handling-arbitrum-outages

Duplicate of #440

thekmj - PriceOracle will use the wrong price if the Chainlink registry returns price outside min/max range

thekmj

medium

PriceOracle will use the wrong price if the Chainlink registry returns price outside min/max range

Summary

Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened to Venus on BSC when LUNA imploded.

Vulnerability Detail

Note there is only a check for price to be non-negative, and not within an acceptable range.

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
    (, int256 price,,,) = registry.latestRoundData(base, quote);
    require(price > 0, "invalid price");

    // Extend the decimals to 1e18.
    return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
}

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

A similar issue is seen here.

Impact

The wrong price may be returned in the event of a market crash. An adversary will then be able to borrow against the wrong price and incur bad debt to the protocol.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

Implement the proper check for each asset. It must revert in the case of bad price.

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
    (, int256 price,,,) = registry.latestRoundData(base, quote);
    require(price >= minPrice && price <= maxPrice, "invalid price"); // @audit use the proper minPrice and maxPrice for each asset

    // Extend the decimals to 1e18.
    return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
}

rvierdiiev - IronBank._isLiquidatable should acrrue interests for all user's markets in order to receive correct collateral/debt balance

rvierdiiev

medium

IronBank._isLiquidatable should acrrue interests for all user's markets in order to receive correct collateral/debt balance

Summary

IronBank._isLiquidatable should acrrue interests for all user's markets in order to receive correct collateral/debt balance. Because it's not done now, that means that user can be liquidated when he actually has enough collateral.

Vulnerability Detail

_isLiquidatable function is used by protocol in order to understand if user has enough collateral to cover his debt. To do that function loops through all user's markets.
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L1065-L1092

        function _isLiquidatable(address user) internal view returns (bool) {
        uint256 liquidationCollateralValue;
        uint256 debtValue;

        address[] memory userEnteredMarkets = allEnteredMarkets[user];
        for (uint256 i = 0; i < userEnteredMarkets.length; i++) {
            DataTypes.Market storage m = markets[userEnteredMarkets[i]];
            if (!m.config.isListed) {
                continue;
            }

            uint256 supplyBalance = m.userSupplies[user];
            uint256 borrowBalance = _getBorrowBalance(m, user);

            uint256 assetPrice = PriceOracleInterface(priceOracle).getPrice(userEnteredMarkets[i]);
            require(assetPrice > 0, "invalid price");
            uint256 liquidationThreshold = m.config.liquidationThreshold;
            if (supplyBalance > 0 && liquidationThreshold > 0) {
                uint256 exchangeRate = _getExchangeRate(m);
                liquidationCollateralValue +=
                    (supplyBalance * exchangeRate * assetPrice * liquidationThreshold) / 1e36 / FACTOR_SCALE;
            }
            if (borrowBalance > 0) {
                debtValue += (borrowBalance * assetPrice) / 1e18;
            }
        }
        return debtValue > liquidationCollateralValue;
    }

As you can see this function doesn't accrue interests for all user's markets. That means that collateral balance and debt balance could be wrong for that markets, because accruing interests has impact on both exchange ratio and borrow index.

Impact

User can be liquidated, when he has enough collateral.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

You need to accrue interests for all markets of user.

tsvetanovv - Every time need to `accrueInterest()` when `repay`

tsvetanovv

high

Every time need to accrueInterest() when repay

Summary

In TxBuilderExtension.sol, functions that use repay tokens to Iron Bank only call accrueInterest() under certain circumstances, and this should not be the case.

Vulnerability Detail

In TxBuilderExtension.sol we have the following function which repays or redems specific token to Iron Bank:

We can see that accrueInterest is called only when amount is the maximum value.

if (stEthAmount == type(uint256).max) {
ย  ย  ย  ย  ย ironBank.accrueInterest(wsteth);
ย  ย .....
ย  ย }

accrueInterest() should be called regardless of the amount to give more accurate data.

Impact

Possible loss of funds due to not accurate token amount

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L356-L368
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L338-L349
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L390-L399

Tool used

Manual Review

Recommendation

To ensure the redemption amount reflects the most up-to-date accrued interest ironBank.accrueInterest should be outside the if statement. This allows for an accurate calculation of the user's current supply balance, considering any interest that has accumulated.

kutugu - Oracle price can be stale when L2 sequencer is down

kutugu

medium

Oracle price can be stale when L2 sequencer is down

Summary

Oracle price can be stale when L2 sequencer is down

Vulnerability Detail

From Chainlink documentation:
Optimistic rollup protocols have a sequencer that executes and rolls up the L2 transactions by batching multiple transactions into a single transaction.
If a sequencer becomes unavailable, it is impossible to access read/write APIs that consumers are using and applications on the L2 network will be down for most users.
This means that if the project does not check if the sequencer is down, it can return stale results.

Note that It is a different issue from checking oracle price freshness.
Because in the case of sharp price fluctuations, the price may be updated several times, although the final price is in freshness, but it may not be the latest price.
There's a similar issue here.

Impact

Medium. Oracle price is stale. Impact borrow, liquidation.

Code Snippet

Tool used

Manual Review

Recommendation

From Chainlink documentation:

        (
            /*uint80 roundID*/,
            int256 answer,
            uint256 startedAt,
            /*uint256 updatedAt*/,
            /*uint80 answeredInRound*/
        ) = sequencerUptimeFeed.latestRoundData();

        // Answer == 0: Sequencer is up
        // Answer == 1: Sequencer is down
        bool isSequencerUp = answer == 0;
        if (!isSequencerUp) {
            revert SequencerDown();
        }

        // Make sure the grace period has passed after the
        // sequencer is back up.
        uint256 timeSinceUp = block.timestamp - startedAt;
        if (timeSinceUp <= GRACE_PERIOD_TIME) {
            revert GracePeriodNotOver();
        }

        // prettier-ignore
        (
            /*uint80 roundID*/,
            int data,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        ) = dataFeed.latestRoundData();

Duplicate of #440

nhtynhty - [INFORMATIONAL/GAS/UX] Use something like an iterable mapping for constant time removal of array elements

nhtynhty

medium

[INFORMATIONAL/GAS/UX] Use something like an iterable mapping for constant time removal of array elements

Summary

The current implementation of deleteElement has a linearly increasing gas cost with the number of elements in the array,
it is ran by users in the _exitMarket function in IronBank.sol as well as a few other spots which are ran by privileged users

Vulnerability Detail

N/A

Impact

Informational/Gas

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/libraries/Arrays.sol#L5
ib-v2/src/libraries/Array.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

library Arrays {
    /**
     * @dev Delete an element from an array.
     * @param self The array to delete from
     * @param element The element to delete
     */
    function deleteElement(address[] storage self, address element) internal {
        uint256 count = self.length;
        for (uint256 i = 0; i < count;) {
            if (self[i] == element) {
                if (i != count - 1) {
                    self[i] = self[count - 1];
                }
                self.pop();
                break;
            }

            unchecked {
                i++;
            }
        }
    }
}

Tool used

N/a

Manual Review

Recommendation

You can use something similar to an 'IterableMapping' to give the deleteElement operation a constant time runtime cost.

You would use a struct that holds the array as well as a mapping(address element => uint256 index) indexOf, this changes the assumptions to an extra sstore when appending or removing from the array, in order to remove the storage read loop, which can be quite expensive

kutugu - delistMarket not deletes userBorrows and userSupplies mapping

kutugu

medium

delistMarket not deletes userBorrows and userSupplies mapping

Summary

delistMarket deletes markets[market], but not deletes userBorrows and userSupplies sub mapping in market struct.
When the market is listed again, the legacy parameter can cause the user to obtain additional funds, breaking the internal accounting system.

Vulnerability Detail

    function hardDelistMarket(address market) external onlyOwner {
        DataTypes.MarketConfig memory config = getMarketConfiguration(market);
        // @audit not check totalSupply, totalCash... 
        require(config.isListed, "not listed");
        require(config.isSupplyPaused() && config.isBorrowPaused(), "not paused");
        require(config.reserveFactor == MAX_RESERVE_FACTOR, "reserve factor not max");
        require(
            config.collateralFactor == 0 && config.liquidationThreshold == 0,
            "collateral factor or liquidation threshold not zero"
        );

        if (config.isPToken) {
            address underlying = PTokenInterface(market).getUnderlying();
            DataTypes.MarketConfig memory underlyingConfig = getMarketConfiguration(underlying);
            // It's possible that the underlying is not listed.
            if (underlyingConfig.isListed && underlyingConfig.pTokenAddress != address(0)) {
                underlyingConfig.pTokenAddress = address(0);
                ironBank.setMarketConfiguration(underlying, underlyingConfig);

                emit MarketPTokenSet(underlying, address(0));
            }
        }

        // @audit entry here
        ironBank.delistMarket(market);

        emit MarketDelisted(market);
    }

    function delistMarket(address market) external onlyMarketConfigurator {
        DataTypes.Market storage m = markets[market];
        require(m.config.isListed, "not listed");

        // @audit not delete userBorrows, userSupplies
        delete markets[market];
        allMarkets.deleteElement(market);

        emit MarketDelisted(market);
    }

    struct Market {
        MarketConfig config;
        uint40 lastUpdateTimestamp;
        uint256 totalCash;
        uint256 totalBorrow;
        uint256 totalSupply;
        uint256 totalReserves;
        uint256 borrowIndex;
        mapping(address => UserBorrow) userBorrows;
        mapping(address => uint256) userSupplies;
    }

delistMarket deletes account data in Market struct, such as totalCash, totalBorrow, totalSupply, totalReserves, but not deletes userBorrows and userSupplies.
There are two cases:

  1. ironBank team notifies in advance without compensation
  2. ironBank team will compensate the user if the market is delist and the user funds will be frozen

For case 1, due to totalCash, totalBorrow, totalSupply, totalReserves are 0, but userBorrows, userSupplies are not 0. The internal accounting system is broken. A simple description of this situation:

  1. MarketA is firstly list. Alice supply x market token, mint x * initialExchangeRate ibToken
  2. IronBank team wants to delist marketA and notifies in advance, but Alice doesn't see
  3. After marketA is delist, Alice's token are frozen
  4. MarketA is list again. Bob supply x market token, mint x * initialExchangeRate ibToken
  5. Alice can withdraw money now, but Bob's token are frozen again
  6. Bad debts continue, like a Ponzi scheme, eventually drive away users.

For case 2, when the market delist user receive an offer of compensation, when it came time to list the market again, users will use these legacy parameters for profit like case 1. So user can receive two profits.

Impact

Medium. Internal accounting system is broken.

Code Snippet

Tool used

Manual Review

Recommendation

For case 1, if ironBank team won't compensate the user, they can add a variable to mark that the market has been delist. Do not change the accounting system of the market so that when the market is listed again, the accounting system can remain normal.
For case 2, if ironBank team will compensate the user, they should delete userBorrows and userSupplies before delistMarket.

sl1 - getPriceFromChainlink function in PriceOracle.sol does not check if the L2 sequencer is down.

sl1

medium

getPriceFromChainlink function in PriceOracle.sol does not check if the L2 sequencer is down.

Summary

When using Chainlink in L2 like arbitrum or optimism, it is important to make sure that prices are not falsly assumed fresh when the sequencer is down. This vulnerability could potentially be exploited by malicious users to gain unfair advantage.

Vulnerability Detail

No check here.

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

Impact

Could potentially be exploited.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

The mitigation is to use the sequencer uptime feed to monitor the sequencer's online status and prevent consumption of price when the sequencer is offline.
Code from official chainlink docs:

constructor() {
        dataFeed = AggregatorV2V3Interface(
            0xC16679B963CeB52089aD2d95312A5b85E318e9d2
        );
        sequencerUptimeFeed = AggregatorV2V3Interface(
            0x4C4814aa04433e0FB31310379a4D6946D5e1D353
        );
    }

    // Check the sequencer status and return the latest data
    function getLatestData() public view returns (int) {
        // prettier-ignore
        (
            /*uint80 roundID*/,
            int256 answer,
            uint256 startedAt,
            /*uint256 updatedAt*/,
            /*uint80 answeredInRound*/
        ) = sequencerUptimeFeed.latestRoundData();

        // Answer == 0: Sequencer is up
        // Answer == 1: Sequencer is down
        bool isSequencerUp = answer == 0;
        if (!isSequencerUp) {
            revert SequencerDown();
        }

        // Make sure the grace period has passed after the
        // sequencer is back up.
        uint256 timeSinceUp = block.timestamp - startedAt;
        if (timeSinceUp <= GRACE_PERIOD_TIME) {
            revert GracePeriodNotOver();
        }

        // prettier-ignore
        (
            /*uint80 roundID*/,
            int data,
            /*uint startedAt*/,
            /*uint timeStamp*/,
            /*uint80 answeredInRound*/
        ) = dataFeed.latestRoundData();

        return data;
    }

https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Duplicate of #440

IceBear - Lack of sufficient validation for chainlink price feed

IceBear

medium

Lack of sufficient validation for chainlink price feed

Summary

Lack of sufficient validation for chainlink price feed

Vulnerability Detail

the round id is not validated.

  function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

Impact

latestRoundData() might return stale results.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

Add roundId check

Duplicate of #9

tsueti_ - Use safeTransfer()/safeTransferFrom() instead of transfer()/transferFrom() for ERC20

tsueti_

medium

Use safeTransfer()/safeTransferFrom() instead of transfer()/transferFrom() for ERC20

Summary

Use safeTransfer()/safeTransferFrom() instead of transfer()/transferFrom() for ERC20

Vulnerability Detail

Impact

It is a good idea to add a require() statement that checks the return value of ERC20 token transfers or to use something like OpenZeppelinโ€™s safeTransfer()/safeTransferFrom() unless one is sure the given token reverts in case of a failure. Failure to do so will cause silent failures of transfers and affect token accounting in contract.

However, using require() to check transfer return values could lead to issues with non-compliant ERC20 tokens which do not return a boolean value. Therefore, itโ€™s highly advised to use OpenZeppelinโ€™s safeTransfer()/safeTransferFrom()

Code Snippet

IBToken.sol#L71-L74

IBToken.sol#L82-L85

Tool used

Manual Review

Recommendation

Use SafeERC20.safeTransfer and safeTransferFrom

yy - Borrower borrow money without having sufficient collateral.

yy

high

Borrower borrow money without having sufficient collateral.

Summary

The code does not explicitly check if a borrower has sufficient collateral before allowing them to borrow funds.

Vulnerability Detail

The code snippet does not include specific checks or validations to ensure that a borrower has sufficient collateral when borrowing funds. Borrowers can borrow money without having enough collateral to cover their debt. Without proper collateral coverage, the borrowing process may proceed.

One of the example:
The borrower supplies collateral assets in market. However, the value of the borrower's collateral in Market A decreases significantly, either due to market fluctuations or a decline in the collateral's value.

As a result, the value of the borrower's collateral in the market falls below the required collateralization ratio for the borrowed funds.

Impact

significant losses for the lending protocol

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L480
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L1065

Tool used

Manual Review

Recommendation

implement a mechanism that performs collateral checks before allowing borrowers to borrow funds. Also, consider implementing a collateralization ratio requirement, where borrowers must maintain a minimum collateral value relative to the borrowed asset value.

rvierdiiev - TxBuilderExtension.redeemNativeToken will not redeem whole amount when user provides type(uint256).max

rvierdiiev

medium

TxBuilderExtension.redeemNativeToken will not redeem whole amount when user provides type(uint256).max

Summary

TxBuilderExtension.redeemNativeToken will not redeem whole amount when user provides type(uint256).max, because it doesn't call accrueInterests before.

Vulnerability Detail

TxBuilderExtension.redeemNativeToken function is used to redeem tokens from IronBank. User can provide type(uint256).max amount when he wants withdraw whole supply amount.
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L275-L283

    function redeemNativeToken(address user, uint256 redeemAmount) internal nonReentrant {
        if (redeemAmount == type(uint256).max) {
            redeemAmount = ironBank.getSupplyBalance(user, weth);
        }
        ironBank.redeem(user, address(this), weth, redeemAmount);
        WethInterface(weth).withdraw(redeemAmount);
        (bool sent,) = user.call{value: redeemAmount}("");
        require(sent, "failed to send native token");
    }

As you can see in case if redeemAmount == type(uint256).max, then ironBank.getSupplyBalance is called to get user's underlying amount that can be withdrawn. The problem here, is that interests are not accrued at this moment, which means that exchange rate can be bigger.

After that function calls ironBank.redeem and provide user's underying amount(without additional interests). And only then interests will be accrued, which will increase exchange rate.

Because of that user will not withdraw whole amount as he wanted and some interests will be still remaining.

Impact

User will not be able to withdraw whole balance, interests can be small, so user will not withdraw them as this will make him pay more on gas.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

You don't need to calculate it in TxBuilderExtension, as this is already done inside IronBank.

Duplicate of #360

p0wd3r - There is a lack of verification for the update time of the oracle data.

p0wd3r

medium

There is a lack of verification for the update time of the oracle data.

Summary

There is a lack of verification for the update time of the oracle data.

Vulnerability Detail

When obtaining the latest price through Chainlink, there is no check on the validity of the updateAt parameter, which may result in obtaining an invalid price.

src/protocol/oracle/PriceOracle.sol

 (, int256 price,,,) = registry.latestRoundData(aggrs[i].base, aggrs[i].quote);
                require(price > 0, "invalid price");

(, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

Chainlink API:

  function latestRoundData(
    address base,
    address quote
  ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

As you can see the updatedAt timestamp is not checked. So the price may be outdated.

Impact

The price may be outdated

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66
src/protocol/oracle/PriceOracle.sol

 (, int256 price,,,) = registry.latestRoundData(aggrs[i].base, aggrs[i].quote);
                require(price > 0, "invalid price");

(, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

Tool used

Manual Review

Recommendation

Add check like this: if (updatedAt < block.timestamp - LIMIT) revert PriceOutdated();

Duplicate of #440

rvierdiiev - TxBuilderExtension.supplyNativeToken uses msg.value

rvierdiiev

medium

TxBuilderExtension.supplyNativeToken uses msg.value

Summary

TxBuilderExtension.supplyNativeToken and repayNativeToken uses msg.value. In case if both of them are used at some set of transactions, then call will revert.

Vulnerability Detail

TxBuilderExtension.supplyNativeToken function sends all msg.value to the weth contract and then supplies it to the IronBank. Also msg.value is used inside repayNativeToken function. In case if both of them are used at some set of transactions, then call will revert.

Impact

Actions set with native token supply and repay will revert.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L252-L256
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L291

Tool used

Manual Review

Recommendation

Use value provided by user for supply and repay native token calls.

BugBusters - `getPriceFromChainlink()` doesn't check If Arbitrum sequencer is down in Chainlink feeds

BugBusters

medium

getPriceFromChainlink() doesn't check If Arbitrum sequencer is down in Chainlink feeds

Summary

When utilizing Chainlink in L2 chains like Arbitrum, it's important to ensure that the prices provided are not falsely perceived as fresh, even when the sequencer is down. This vulnerability could potentially be exploited by malicious actors to gain an unfair advantage.

Vulnerability Detail

If the Arbitrum Sequencer goes down, oracle data will not be kept up to date, and thus could become stale. However, users are able to continue to interact with the protocol directly through the L1 optimistic rollup contract. You can review Chainlink docs on L2 Sequencer Uptime Feeds for more details on this.

As a result, users may be able to use the protocol while oracle feeds are stale. This could cause many problems, but as a simple example:

A user has an account with 100 tokens, valued at 1 ETH each, and no borrows
The Arbitrum sequencer goes down temporarily
While it's down, the price of the token falls to 0.5 ETH each
The current value of the user's account is 50 ETH, so they should be able to borrow a maximum of 200 ETH
But because of the stale price, the protocol lets them borrow 400 ETH

Impact

If the Arbitrum sequencer goes down, the protocol will allow users to continue to operate at the previous (stale) rates that could potentially be exploited by malicious actors to gain an unfair advantage.

Code Snippet

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote); //@audit chainlink issues
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

code example of Chainlink:
https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Duplicate of #440

tsvetanovv - Oracle will return the wrong price for asset if underlying aggregator hits `minAnswer`

tsvetanovv

medium

Oracle will return the wrong price for asset if underlying aggregator hits minAnswer

Summary

getPriceFromChainlink() and _setAggregators() will returns the wrong price for asset if underlying aggregator hits minAnswer

Vulnerability Detail

Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened toย Venus on BSC when LUNA imploded.

Whenย latestRoundData()ย is called it request data from the aggregator. The aggregator has a minPrice and a maxPrice. If the price falls below the minPrice instead of reverting it will just return the min price.

Impact

In the event that an asset crashes the protocol can be manipulated to give out loans at an inflated price

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L107

Tool used

Manual Review

Recommendation

getPriceFromChainlink() and _setAggregators() should check the returned answer against the minPrice/maxPrice and revert if the answer is outside of the bounds:

if (price >= maxPrice or price <= minPrice) revert();

Duplicate of #25

tsvetanovv - An attacker can apply grieving attack by preventing users from interacting with `repay` function

tsvetanovv

medium

An attacker can apply grieving attack by preventing users from interacting with repay function

Summary

In IronBank.sol we have _repay()function. This function repays an amount of assets to Iron Bank.

function _repay(DataTypes.Market storage m, address from, address to, address market, uint256 amount)
ย  ย  ย  ย  internal
ย  ย  ย  ย  returns (uint256)
ย  ย  {
ย  ย  ย  ย  uint256 borrowBalance = _getBorrowBalance(m, to);
ย  ย  ย  ย  if (amount == type(uint256).max) {
ย  ย  ย  ย  ย  ย  amount = borrowBalance;
ย  ย  ย  ย  }

ย  ย  ย  ย  require(amount <= borrowBalance, "repay too much"); 

ย  ย  ย  ย  uint256 newUserBorrowBalance;
ย  ย  ย  ย  uint256 newTotalBorrow;
ย  ย  ย  ย  unchecked {
ย  ย  ย  ย  ย  ย  newUserBorrowBalance = borrowBalance - amount;
ย  ย  ย  ย  ย  ย  // Underflow not possible: amount <= userBorrow <= totalBorrow
ย  ย  ย  ย  ย  ย  newTotalBorrow = m.totalBorrow - amount;
ย  ย  ย  ย  }

ย  ย  ย  ย  // Update storage.
ย  ย  ย  ย  m.userBorrows[to].borrowBalance = newUserBorrowBalance;
ย  ย  ย  ย  m.userBorrows[to].borrowIndex = m.borrowIndex;
ย  ย  ย  ย  m.totalCash += amount;
ย  ย  ย  ย  m.totalBorrow = newTotalBorrow;

ย  ย  ย  ย  // Check if need to exit the market.
ย  ย  ย  ย  if (m.userSupplies[to] == 0 && newUserBorrowBalance == 0) {
ย  ย  ย  ย  ย  ย  _exitMarket(market, to);
ย  ย  ย  ย  }

ย  ย  ย  ย  IERC20(market).safeTransferFrom(from, address(this), amount);

ย  ย  ย  ย  emit Repay(market, from, to, amount, newUserBorrowBalance, newTotalBorrow);

ย  ย  ย  ย  return amount;
ย  ย  }

The function is internal and is called in repay() and liquidate().

Vulnerability Detail

The problem here is whenever a user is going to use _repay() function, malicious user (for example Bob) can apply a grieving attack by preventing users from interacting with it.
Bob could prevent the user from paying her debt fully by just repaying a very small amount of the user's debt in advance and as a result honest user transaction not pass.

984: require(amount <= borrowBalance, "repay too much");

For example:

  • Alice wants to use repay function with full borrowBalance 100 tokens and pass amount=100.
  • Bob observes mempols and do font-run grieving attack with 1 wei.
  • Alice the transaction could not go through because of require check:
  • require(amount <= borrowBalance, "repay too much")

Bob can apply this attack for all other users who are going to repay their debt fully.

Impact

A malicious user can prevent an honest user from using a key function.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L975-L1010
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L460C5-L470
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L480-L510

Tool used

Manual Review

Recommendation

Instead of this check:

ย  ย  ย  ย  if (amount == type(uint256).max) {
ย  ย  ย  ย  ย  ย  amount = borrowBalance;
ย  ย  ย  ย  }

Change it like this:

ย  ย  ย  ย  if (amount > borrowBalance) {
ย  ย  ย  ย  ย  ย  amount = borrowBalance;
ย  ย  ย  ย  }

tsvetanovv - `redeemNativeToken()` missing to call `accrueInterest` before set `redeemAmount`

tsvetanovv

high

redeemNativeToken() missing to call accrueInterest before set redeemAmount

Summary

In TxBuilderExtension.sol we have redeemNativeToken() function:

ย  ย  function redeemNativeToken(address user, uint256 redeemAmount) internal nonReentrant {
ย  ย  ย  ย  if (redeemAmount == type(uint256).max) {
ย  ย  ย  ย  ย  ย  redeemAmount = ironBank.getSupplyBalance(user, weth);
ย  ย  ย  ย  }
ย  ย  ย  ย  ironBank.redeem(user, address(this), weth, redeemAmount);
ย  ย  ย  ย  WethInterface(weth).withdraw(redeemAmount);
ย  ย  ย  ย  (bool sent,) = user.call{value: redeemAmount}("");
ย  ย  ย  ย  require(sent, "failed to send native token");
ย  ย  }

This function redeems the wrapped native token and unwraps it to the user.

Vulnerability Detail

The problem here if redeemAmount == type(uint256).max the function should call

ironBank.accrueInterest(weth)

in order to calculate redeemAmount correctly.

And then call:

277: redeemAmount = ironBank.getSupplyBalance(user, weth);

By conditionally calling accrueInterest when redeemAmount is maximum, the function guarantees that the redemption amount accurately reflects the current supply balance, including any accrued interest.

You can see how you did it in redeemStEth and redeemPToken:

ย  ย  ย  ย  if (amount == type(uint256).max) {
ย  ย  ย  ย  ย  ย  ironBank.accrueInterest(pToken);
ย  ย  ย  ย  ย  ย  amount = ironBank.getSupplyBalance(user, pToken);
ย  ย  ย  ย  }

Impact

Possible loss of funds due to the wrong redeemAmount

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L275-L283

Tool used

Manual Review

Recommendation

To ensure the redemption amount reflects the most up-to-date accrued interest, it is generally advisable to include an accrueInterest step before retrieving the supply balance. This allows for an accurate calculation of the user's current supply balance, considering any interest that has accumulated.

ย  ย  ย  ย  if (redeemAmount == type(uint256).max) {
	ย  ย  ย  ย  ironBank.accrueInterest(weth);
ย  ย  ย  ย  ย  ย  redeemAmount = ironBank.getSupplyBalance(user, weth);
ย  ย  ย  ย  }

tsvetanovv - `liquidationBonus` with a minimum value of `MIN_LIQUIDATION_BONUS` will always revert

tsvetanovv

medium

liquidationBonus with a minimum value of MIN_LIQUIDATION_BONUS will always revert

Summary

liquidationBonus with a minimum value of 100% will always revert

Vulnerability Detail

In MarketConfigurator.sol we have configureMarketAsCollateral and adjustMarketLiquidationBonus() functions. The first function allows the owner to configure a market as collateral and the second is to adjust the liquidation bonus of a market.
The problem here is that both functions will revert when the minimum allowed value is set.
The minimum allowed value is in Constants.sol:

12: uint16 internal constant MIN_LIQUIDATION_BONUS = 10000; // 100%

Both Functions have the following check:

require(
ย  ย  ย  ย  ย  ย  liquidationBonus > MIN_LIQUIDATION_BONUS && liquidationBonus <= MAX_LIQUIDATION_BONUS, ย 
ย  ย  ย  ย  ย  ย  "invalid liquidation bonus"
ย  ย  ย  ย  );

ะžmitted is to add the sign greater than or equal to.

As you can see the function will not allow to set the liquidationBonus value to 100% (that is 10000).

Impact

It is impossible to settle a liquidation bonus of 100%

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/MarketConfigurator.sol#L141
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/MarketConfigurator.sol#L238
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/Constants.sol#L12

Tool used

Manual Review

Recommendation

Change:

liquidationBonus > MIN_LIQUIDATION_BONUS && liquidationBonus <= MAX_LIQUIDATION_BONUS

To:

liquidationBonus >= MIN_LIQUIDATION_BONUS && liquidationBonus <= MAX_LIQUIDATION_BONUS

tsvetanovv - Price Oracle could get a stale price

tsvetanovv

medium

Price Oracle could get a stale price

Summary

No check for round completeness could lead to stale prices and wrong price return value, or outdated price. The functions that rely on accurate price feed might not work as expected, which sometimes can lead to fund loss.

Vulnerability Detail

The oracle wrapper getPriceFromChainlink() call out to an oracle with latestRoundData() to get the price of some token. Although the returned timestamp is checked, there is no check for round completeness.

According to Chainlink's documentation, this function does not error if no answer has been reached but returns 0 or outdated round data. The external Chainlink oracle, which provides index price information to the system, introduces risk inherent to any dependency on third-party data sources. For example, the oracle could fall behind or otherwise fail to be maintained, resulting in outdated data being fed to the index price calculations. Oracle reliance has historically resulted in crippled on-chain systems, and complications that lead to these outcomes can arise from things as simple as network congestion.

The same problem is in _setAggregators() function in PriceOracle.sol

Impact

If there is a problem with chainlink starting a new round and finding consensus on the new value for the oracle (e.g. chainlink nodes abandon the oracle, chain congestion, vulnerability/attacks on the chainlink system) consumers of this contract may continue using outdated stale data (if oracles are unable to submit no new round is started).

This could lead to stale prices and wrong price return value, or outdated price.

As a result, the functions that rely on accurate price feed might not work as expected, which sometimes can lead to fund loss. The impacts vary and depend on the specific situation like the following:

  • incorrect liquidation
  • some users could be liquidated when they should not
  • no liquidation is performed when there should be
  • wrong price feed

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

Validate data feed for round completeness:

     (uint80 roundID, int256 price, , uint256 timestamp, uint80 answeredInRound) = registry.latestRoundData(base, quote);
		
     require(answeredInRound >= roundID, "round not complete");

Duplicate of #9

n1punp - No check for active Optimism & Arbitrum Sequencer in Chainlink Oracle (oracle integration issues)

n1punp

medium

No check for active Optimism & Arbitrum Sequencer in Chainlink Oracle (oracle integration issues)

Summary

No check for active Optimism & Arbitrum Sequencer in Chainlink Oracle (oracle integration issues)

Vulnerability Detail

If these L2 sequencers were to go offline, the oracle may return invalid or stale prices.

Impact

  • Invalid or stale prices may be returned for the protocol

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L42-L58

Tool used

Manual Review

Recommendation

Duplicate of #440

rvierdiiev - Liquidators can be not interested in liquidation of small debts

rvierdiiev

medium

Liquidators can be not interested in liquidation of small debts

Summary

Liquidators can be not interested in liquidation of small debts. So this debts will remain in the protocol and will create bad debt.

Vulnerability Detail

In order to liquidate debt position, IronBank has liquidate function. This function allows liquidator to provide amount he wants to liquidate on behalf of debtor.

This will be only done, when liquidation is attractive to liquidator, which means that he will earn smth from the call.
IronBank uses liquidationBonus to incentivize liquidators.

But in case if the debt is too small(for example 5$), that means that it can be not attractive for the liquidator to liquidate debt, because he will spend more funds as gas payment than he will earn. That means that such debts usually will not liquidated and will remain in the system and create bad debt.

What is worse, is that some liquidators can liquidate accounts not fully, but leave some small amount, exactly to create bad debt, that will not be liquidated by anyone else.

I believe that this is high severity issue, because borrowers, interested repayers and liquidators can make system insolvent, which will cause stakers to redeem.

Impact

System will face bad debt, which can cause insolvency

Code Snippet

Provided

Tool used

Manual Review

Recommendation

I guess that you should provide some limits. One of them is to disallow to borrow small amount(minimum limit should be reached). Another is to disallow repayment that again leaves small debt(still should be bigger than limit or repaid fully). And sam ething for the liquidation, small debt can't be leaved.
Using this approach, your debts will always be attractive for liquidators.

ravikiran.web3 - Protocol contracts use floating pragma, instead they should be locked to a specific version of compiler

ravikiran.web3

medium

Protocol contracts use floating pragma, instead they should be locked to a specific version of compiler

Summary

All protocol contracts uses floating version like pragma solidity ^0.8.0. This exposes to potential bugs that might effect the contract negatively. Floating pragma can be used for contracts that are intended to be used by other developers, but specific protocol contracts should always lock to a specific version of compiler in the test environment and conduct testing with the compiler and target the same compiler on deployment. It is a known issue and listed in swcregistry.

Instead, they should be locked to specific version of compiler like below as example.
pragma solidity 0.8.17;

Vulnerability Detail

With floating pragma, the dev team will loose control of how the contract wil behave over its lifetime. Locking it to a specific version ensure the behaviour is tested first hand and that behaviour should remain.

Impact

CWE-664: Improper Control of a Resource Through its Lifetime

Code Snippet

The below is an example reference, this pragma is observed across all contract files in the protocol.

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L3

pragma solidity ^0.8.0;

pragma solidity ^0.8.0;

Tool used

Manual Review

Recommendation

  1. Update all the core contracts to specific version of the complier.
    As example, something like below.
    pragma solidity 0.8.17;

  2. Perform thorough testing of the suite to ensure the protocol behaviour is validated for all scenarios.

tsvetanovv - Missing check for active L2 Sequencer in `PriceOracle.sol`

tsvetanovv

medium

Missing check for active L2 Sequencer in PriceOracle.sol

Summary

In Q&A we see:

On what chains are the smart contracts going to be deployed?

  • mainnet, Arbitrum, Optimism

You should always check for sequencer availability when using Chainlink's Arbitrum or Optimism price feeds or another layer 2 chains.

Vulnerability Detail

Optimistic rollup protocols move all execution off the layer 1 (L1) Ethereum chain, complete execution on a layer 2 (L2) chain, and return the results of the L2 execution back to the L1. These protocols have aย sequencerย that executes and rolls up the L2 transactions by batching multiple transactions into a single transaction.

If a sequencer becomes unavailable, it is impossible to access read/write APIs that consumers are using and applications on the L2 network will be down for most users without interacting directly through the L1 optimistic rollup contracts. The L2 has not stopped, but it would be unfair to continue providing service on your applications when only a few users can use them.

Impact

If the Arbitrum Sequencer goes down, oracle data will not be kept up to date, and thus could become stale.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L12

Tool used

Manual Review

Recommendation

Check this example -> https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code

Duplicate of #440

IceBear - Lack of slippage control

IceBear

medium

Lack of slippage control

Summary

In UniswapExtension.sol, the functions uniV3ExactOutputInternal() and uniV3ExactInputInternal() lack slippage control.

Vulnerability Detail

Calling uniV3ExactOutputInternal() and uniV3ExactInputInternal() can lead to slippage.

Impact

The slippage control is set to the maximum and minimum of the allowed max and min sqrt ratio,without precise slippage control, swaps done by user are susceptible to sandwich attacks.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/UniswapExtension.sol#L715-L736
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/UniswapExtension.sol#L744-L761

Tool used

Manual Review

Recommendation

Recommend following uniswap's way of implementing the expected minimum amount out check.

harisnabeel - Insufficient Checks on Chainlink's Returned Prices

harisnabeel

medium

Insufficient Checks on Chainlink's Returned Prices

Summary

Chainlink's latestRoundData() is used but there is no check if the return value indicates stale data. This could lead to stale prices according to the Chainlink documentation:

https://docs.chain.link/docs/historical-price-data/#historical-rounds

Vulnerability Detail

The PriceOracle.getPrice() calls an internal function getPriceFromChainlink() that uses Chainlink's latestRoundData() to get the latest price. However, there is no check if the return value indicates stale data.

Impact

PriceOracle.getPrice() is used in critical parts like IronBank._getAccountLiquidity() and IronBank._isLiquidatable() where the staleness of price can lead to calculate invalid collateralValue and debtValue resulting in unexpected behavior.

Code Snippet

PriceOracle.getPriceFromChainlink()

    function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

Tool used

Manual Review

Recommendation

Consider adding checks for stale data. e.g

    function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
       (uint80 roundId, int256 price, , uint256 updatedAt, uint80 answeredInRound) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

        require(answeredInRound >= roundId, "Price stale");
        require(block.timestamp - updatedAt < PRICE_ORACLE_STALE_THRESHOLD, "Price round incomplete");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

Duplicate of #9

rvierdiiev - MarketConfigurator.hardDelistMarket function should accrueInterests

rvierdiiev

medium

MarketConfigurator.hardDelistMarket function should accrueInterests

Summary

MarketConfigurator.hardDelistMarket function should accrueInterests, before the ironBank.delistMarket call.

Vulnerability Detail

MarketConfigurator.hardDelistMarket function will be called in order to close market. That means that the market is not will be working anymore and anyone can't call supply/redeem/repay functions.

From discord channel:

The delistMarket function will be called after the market has been paused and the collateral factor has been set to zero by the governance. The Iron Bank team will assess the impact on the remaining suppliers of the market when the collateral factor is set to zero. If the total borrow amount is relatively low, we will cover all debts using our protocol reserves.

So protocol is going to cover all debts by protocol reserves. But in order to have correct debt amount, accrueInterest should be called. And it can be called only, when market is active.

But MarketConfigurator.hardDelistMarket function doesn't accrue interests before ironBank.delistMarket call and ironBank.delistMarket doesn't call accrueInterests as well.

Impact

Incorrect debt value will be saved.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L286

Tool used

Manual Review

Recommendation

Call accrueInterests in MarketConfigurator.hardDelistMarket function.

thekmj - Chainlink oracle may return stale data

thekmj

medium

Chainlink oracle may return stale data

Summary

PriceOracle has no check for staleness of returned price.

Vulnerability Detail

In the function getPriceFromChainlink, there is no check for whether the returned price is stale or not.

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
    (, int256 price,,,) = registry.latestRoundData(base, quote);
    require(price > 0, "invalid price");

    // Extend the decimals to 1e18.
    return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
}

This check must be performed by the protocol, according to the Chainlink docs.

Impact

PriceOracle is not reliable enough to handle the corner cases when the data might be stale.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

Implement the proper check as per this example.

Duplicate of #9

BugHunter101 - PToken.absorb() does not have auth, it may cause user loss fund

BugHunter101

high

PToken.absorb() does not have auth, it may cause user loss fund

Summary

PToken.absorb() does not have auth. If someone transfers to underlying, the attacker can earn the difference by calling absorb continuously at this time

Vulnerability Detail

As we can see, the absorb() does not check the user 's balance of IERC20(underlying).If someone transfers to underlying, the attacker can earn the difference by calling absorb continuously at this time

    function absorb(address user) public {//@audit no check
        uint256 balance = IERC20(underlying).balanceOf(address(this));

        uint256 amount = balance - totalSupply();
        _mint(user, amount);
    }

Impact

If someone transfers to underlying, the attacker can earn the difference by calling absorb continuously at this time

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/token/PToken.sol#L63

Tool used

Manual Review

Recommendation

Adding limit such as check user.balance.

IceBear - Flashloan end result isn't controlled

IceBear

medium

Flashloan end result isn't controlled

Summary

Flashloan end result isn't controlled, there is no balance check before and after the flash loan execution.

Vulnerability Detail

Does not assert that the balance is consistent before and after the flashloan which may be exploited.
similar finding: sherlock-audit/2023-01-ajna-judging#101

Impact

If certain edge cases are met, an attacker could steal the sum of tokens flashloaned.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/flashLoan/FlashLoan.sol#L97-L122

Tool used

Manual Review

Recommendation

Recommend checking balance before and after flashloaning and revert if the correct number of tokens aren't returned to add an extra layer of protection for the protocol.

rvierdiiev - TxBuilderExtension.repayStEth can revert because wstEthAmount can be bigger than borrowBalance

rvierdiiev

medium

TxBuilderExtension.repayStEth can revert because wstEthAmount can be bigger than borrowBalance

Summary

TxBuilderExtension.repayStEth can revert because wstEthAmount can be bigger than borrowBalance

Vulnerability Detail

TxBuilderExtension.repayStEth function uses borrowBalance as amount to repay in case when stEthAmount == type(uint256).max.
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L356-L368

    function repayStEth(address user, uint256 stEthAmount) internal nonReentrant {
        if (stEthAmount == type(uint256).max) {
            ironBank.accrueInterest(wsteth);
            uint256 borrowBalance = ironBank.getBorrowBalance(user, wsteth);
            stEthAmount = WstEthInterface(wsteth).getStETHByWstETH(borrowBalance) + 1; // add 1 to avoid rounding issue
        }

        IERC20(steth).safeTransferFrom(user, address(this), stEthAmount);
        IERC20(steth).safeIncreaseAllowance(wsteth, stEthAmount);
        uint256 wstEthAmount = WstEthInterface(wsteth).wrap(stEthAmount);
        IERC20(wsteth).safeIncreaseAllowance(address(ironBank), wstEthAmount);
        ironBank.repay(address(this), user, wsteth, wstEthAmount);
    }

This borrowBalance is then converted to stEth amount. Also value of 1 is added to stEthAmount in order to avoid rounding issue.

After that this stEthAmount amount will be wrapped to wstEthAmount and it will be repaid using ironBank.repay(address(this), user, wsteth, wstEthAmount).

repay function doesn't allow to repay more than user's debt. That means that in case if wstEthAmount != borrowBalance, then function will revert.

And this is possible actually, because of that +1 to stEthAmount amount.

Impact

Repay will fail.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

You need to check if you need round up or no.

BugBusters - `PriceOrcale` will return the wrong price for asset if underlying aggregator hits minAnswer

BugBusters

medium

PriceOrcale will return the wrong price for asset if underlying aggregator hits minAnswer

Summary

Chainlink aggregators have a built in circuit breaker if the price of an asset goes outside of a predetermined price band. The result is that if an asset experiences a huge drop in value (i.e. LUNA crash) the price of the oracle will continue to return the minPrice instead of the actual price of the asset. This would allow user to continue borrowing with the asset but at the wrong price. This is exactly what happened to Venus on BSC when LUNA imploded.

Vulnerability Detail

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote); //@audit chainlink issues
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

ChainlinkFeed latestRoundData pulls the associated aggregator and requests round data from it. ChainlinkAggregators have minPrice and maxPrice circuit breakers built into them. This means that if the price of the asset drops below the minPrice, the protocol will continue to value the token at minPrice instead of it's actual value. This will allow users to take out huge amounts of bad debt and bankrupt the protocol.

Example:
TokenA has a minPrice of $1. The price of TokenA drops to $0.10. The aggregator still returns $1 allowing the user to borrow against TokenA as if it is $1 which is 10x it's actual value.

Note:
Chainlink oracles are used a just one piece of the OracleAggregator system and it is assumed that using a combination of other oracles, a scenario like this can be avoided. However this is not the case because the other oracles also have their flaws that can still allow this to be exploited. As an example if the chainlink oracle is being used with a UniswapV3Oracle which uses a long TWAP then this will be exploitable when the TWAP is near the minPrice on the way down. In a scenario like that it wouldn't matter what the third oracle was because it would be bypassed with the two matching oracles prices. If secondary oracles like Band are used a malicious user could DDOS relayers to prevent update pricing. Once the price becomes stale the chainlink oracle would be the only oracle left and it's price would be used.

Impact

In the event that an asset crashes (i.e. LUNA) the protocol can be manipulated to give out loans at an inflated price

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L107-L108

Tool used

Manual Review

Recommendation

PriceOrcale should check the returned answer against the minPrice/maxPrice and revert if the answer is outside of the bounds:

function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote); 
        require(price > 0, "invalid price");
        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

+

if (price >= maxPrice or answer <= minPrice) revert();

Duplicate of #25

BugBusters - `PriceOracle's` `latestRoundData` might return stale or incorrect results

BugBusters

medium

PriceOracle's latestRoundData might return stale or incorrect results

Summary

ChainlinkOracle should use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough to be used.

Vulnerability Detail

In the current implementation of PriceOracle.sol#getPriceFromChainlink(), there is no freshness check. This could lead to stale prices being used.

If the market price of the token drops very quickly ("flash crashes"), and Chainlink's feed does not get updated in time, the smart contract will continue to believe the token is worth more than the market value.

Chainlink also advise developers to check for the updatedAt before using the price:

Your application should track the latestTimestamp variable or use the updatedAt value from the latestRoundData() function to make sure that the latest answer is recent enough for your application to use it. If your application detects that the reported answer is not updated within the heartbeat or within time limits that you determine are acceptable for your application, pause operation or switch to an alternate operation mode while identifying the cause of the delay.

And they have this heartbeat concept:

Chainlink Price Feeds do not provide streaming data. Rather, the aggregator updates its latestAnswer when the value deviates beyond a specified threshold or when the heartbeat idle time has passed. You can find the heartbeat and deviation values for each data feed at data.chain.link or in the Contract Addresses lists.

The Heartbeat on Arbitrum is usually 1h.

Source: https://docs.chain.link/docs/arbitrum-price-feeds/

Impact

A stale price can cause the malfunction of multiple features across the protocol:

ChainlinkOracle.sol#getPrice() is used to calculate the value of various Tokens. If the price is not accurate, it will lead to a deviation in the Token price and affect the calculation of asset prices.

Stale asset prices can lead to bad debts to the protocol as the collateral assets can be overvalued, and the collateral value can not cover the loans.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L107-L108

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

Tool used

Manual Review

Recommendation

Consider adding the missing freshness check for stale price

Duplicate of #9

rvierdiiev - PriceOracle.getPrice doesn't check for stale price

rvierdiiev

medium

PriceOracle.getPrice doesn't check for stale price

Summary

PriceOracle.getPrice doesn't check for stale price. As result protocol can make decisions based on not up to date prices, which can cause loses.

Vulnerability Detail

PriceOracle.getPrice function is going to provide asset price using chain link price feeds.
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L66-L72

    function getPriceFromChainlink(address base, address quote) internal view returns (uint256) {
        (, int256 price,,,) = registry.latestRoundData(base, quote);
        require(price > 0, "invalid price");

        // Extend the decimals to 1e18.
        return uint256(price) * 10 ** (18 - uint256(registry.decimals(base, quote)));
    }

This function doesn't check that prices are up to date. Because of that it's possible that price is not outdated which can cause financial loses for protocol.

Impact

Protocol can face bad debt.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

You need to check that price is not outdated by checking round timestamp.

BugHunter101 - TxBuilderExtension.onDeferredLiquidityCheck() does not use `payable`. This will cause errors when calling some functions that require fees

BugHunter101

medium

TxBuilderExtension.onDeferredLiquidityCheck() does not use payable. This will cause errors when calling some functions that require fees

Summary

TxBuilderExtension.onDeferredLiquidityCheck() does not use payable. This will cause errors when calling some functions that require fees

Vulnerability Detail

As we can see, onDeferredLiquidityCheck() is a callback function and it will call executeInternal()

function onDeferredLiquidityCheck(bytes memory encodedData) external override {
        require(msg.sender == address(ironBank), "untrusted message sender");

        (address initiator, Action[] memory actions, uint256 index) =
            abi.decode(encodedData, (address, Action[], uint256));
        executeInternal(initiator, actions, index);
    }

the executeInternal() will be called according to the parameter actions specification.
And then, there have some function which require fees . such borrowNativeToken

function borrowNativeToken(address user, uint256 borrowAmount) internal nonReentrant {
        ironBank.borrow(user, address(this), weth, borrowAmount);
        WethInterface(weth).withdraw(borrowAmount);
        (bool sent,) = user.call{value: borrowAmount}("");
        require(sent, "failed to send native token");
    }

So, when we call borrowNativeToken() will cause error. TxBuilderExtension.onDeferredLiquidityCheck() does not use payable. This will cause errors when calling some functions that require fees.

Impact

This will cause errors when calling some functions that require fees

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/extensions/TxBuilderExtension.sol#L105

Tool used

Manual Review

Recommendation

Adding payable for this function

n1punp - Attacker can borrow non-zero token amount without providing any collateral

n1punp

high

Attacker can borrow non-zero token amount without providing any collateral

Summary

Attacker can borrow non-zero token amount without providing any collateral

Vulnerability Detail

  • _getAccountLiquidity rounds debt value down, so the debt value is actually lower than what it should be, leading to a possible borrowing without any collateralization.
  • To be more precise, take DAI for example. When oracle reports price < 1e18 (less than $1), then borrowing 1 wei of the token will round the debt value down to 0, allowing the attacker to borrow nonzero amount without actually providing any collateral. This can be repeated as much as one desired.
  • Here's the modified TestBorrow.t.sol test case:
contract BorrowTest is ... {
  ...
  int256 internal constant market2Price = 0.9e8;
  ...

   function testBorrowFree() public {
        uint256 market1SupplyAmount = 100e18;
        uint256 market2BorrowAmount = 500e18;

        vm.startPrank(user2);
        market2.approve(address(ib), market2BorrowAmount);
        ib.supply(user2, user2, address(market2), market2BorrowAmount);
        vm.stopPrank();

        // test borrow from multiple accounts
        for(uint160 i = 1; i < 1000; i++) {
            address addr = address(i);
            vm.prank(addr);
            ib.borrow(addr, addr, address(market2), 1);
        }

        /**
         * collateral value = 0
         * borrowed value = 0 (even though borrowed sth)
         */
        (uint256 collateralValue, uint256 debtValue) = ib.getAccountLiquidity(user1);
        assertEq(collateralValue, 0);
        assertEq(debtValue, 0);
    }

This is the result:

Running 1 test for test/TestBorrow.t.sol:BorrowTest
[PASS] testBorrowFree() (gas: 162751618)

Impact

  • Attacker can borrow tokens without providing any collateral, creating an undesirable state.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/IronBank.sol#L1054

Tool used

Manual Review

Recommendation

  • Round debtValue calculation up in favor of the protocol.

IceBear - Deprecated safeApprove() function

IceBear

medium

Deprecated safeApprove() function

Summary

Deprecated safeApprove() function

Vulnerability Detail

The OpenZeppelin ERC20 safeApprove() function has been deprecated, as seen in the comments of the OpenZeppelin code.

Impact

safeApprove() is Deprecated because has issues similar to the ones found in {IERC20-approve}, and its usage is discouraged.

Read More: SafeERC20.safeApprove() Has unnecessary and unsecure added behavior
OpenZeppelin/openzeppelin-contracts#2219

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/flashLoan/FlashLoan.sol#L108

Tool used

Manual Review

Recommendation

As suggested by the OpenZeppelin comment, replace safeApprove() with safeIncreaseAllowance() or safeDecreaseAllowance() instead.

Duplicate of #420

n1punp - PriceOracle.sol will not work on Optimism & Arbitrum network

n1punp

high

PriceOracle.sol will not work on Optimism & Arbitrum network

Summary

The current implementation of PriceOracle.sol will not work on Optimism & Arbitrum network.

Vulnerability Detail

The implementation of PriceOracle.sol relies on the existence of Chainlink's FeedRegistry, which is currently only available on Ethereum mainnet.

Impact

  • Price oracle will not work as is. It'll require a new implementation that requires manual setup for each aggregator source.
  • The protocol will not function correctly without the price oracle.

Code Snippet

https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/oracle/PriceOracle.sol#L67

Tool used

Manual Review

Recommendation

  • For Optimism & Arbitrum, have a different implementation that doesn't rely on the FeedRegistry, e.g. by having a mapping from token address you want to query to map to the aggregator price source.

Duplicate of #191

rvierdiiev - In case if market is delisted then suppliers can't redeem their ibToken

rvierdiiev

medium

In case if market is delisted then suppliers can't redeem their ibToken

Summary

In case if market is delisted then suppliers can't redeem their ibToken

Vulnerability Detail

IronBank.delistMarket function will mark market as not active and remove the state of market.
Protocol team said:

The delistMarket function will be called after the market has been paused and the collateral factor has been set to zero by the governance. The Iron Bank team will assess the impact on the remaining suppliers of the market when the collateral factor is set to zero. If the total borrow amount is relatively low, we will cover all debts using our protocol reserves.

In case if delistMarket will be called on market, where totalSupply is not 0, then it will be not possible for holders to redeem their shares.

Impact

Stake holders will not be able to redeem.

Code Snippet

Provided above

Tool used

Manual Review

Recommendation

Think how they can redeem in such case.

kutugu - Oracle price not check for accuracy

kutugu

medium

Oracle price not check for accuracy

Summary

Not check oracle price accuracy.

Vulnerability Detail

Oracle prices can be outdated and inaccurate, and oracle prices have update cycles and threshold intervals. You should check the accuracy of the relevant data rather than simply reading the return value.
This is closely related to the liquidation of users' funds, and the stale price may be used by the liquidator, causing losses to users.

Impact

Medium. Oracle stale price may cause user to liquidate.

Code Snippet

Tool used

Manual Review

Recommendation

Check that the timestamp with the return price is within the acceptable threshold

Duplicate of #9

XDZIBEC - XDZIBEC- Unauthorized Contract Pausing in Pausable Contract

XDZIBEC

medium

XDZIBEC- Unauthorized Contract Pausing in Pausable Contract

Summary

The _pause() function in Contract has a vulnerability that allows any account to pause the contract, even if they are not authorized. This can lead to a disruption in the contract's functionality and prevent legitimate users from interacting with it.

Vulnerability Detail

there is a vulnerability in _pause() function:

    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    /**
     * @dev Returns to normal state.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}

the function _pause() does not check if the caller is an authorized account.
This means that any account could pause the contract, even if they are not authorized to do so.
This allow an attacker to prevent users from interacting with the contract.

Impact

the bug in _pause() function could be exploited by an attacker can lead to disrupt the functionality of the contract and prevent users from interacting with it.

Code Snippet

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/09329f8a18f08df65863a5060f6e776bf7fccacf/contracts/security/Pausable.sol#L89

Tool used

Manual Review

Recommendation

modify the_pause() function to include a check that only authorized accounts can pause the contract. This can be achieved by implementing access control mechanisms, such as utilizing the Ownable contract from OpenZeppelin or implementing a custom access control solution.
By implementing proper access controls, the contract can ensure that only authorized accounts have the ability to pause or unpause the contract, maintaining the integrity and usability of the system.

ravikiran.web3 - CreditLimitManager's Owner role's powers can result in liquidation of creditAccount. Guardian role should be segregated

ravikiran.web3

medium

CreditLimitManager's Owner role's powers can result in liquidation of creditAccount. Guardian role should be segregated

Summary

In the CreditLimitManager contract, pauseCreditLimit() function can be called by both Owner and Guardian. In addition, owner can call setCreditLimit() for any account.

The pauseCreditLimit is designed for CreditAccount() to ensure further borrowing is disabled, while making sure that the account is not liquidated. As owner can call both functions, calling setCreditLimit() by mistake can result in liquidation of unintended positions.

This implementation also does not segregate the roles and responsibilities for Owner and Guardian.
Owner as super power
a) should be able to set Guardian.
b) able to set creditLimit for non credit accounts

and leave the pausing of credit to Guardian role as per the documentation on the website.

role of Guardian
Reduce credit limit to $1 for a specific account, preventing it to borrow further without making it a non-credit-limit account

Vulnerability Detail

Incorrect choosing of function by owner to set creditlimit on an account can result in liquidation of unintended account. Also, role are not properly segregated.

Impact

Unintended Liquidation of CreditAccount.

Code Snippet

Pausing Credit Limit for Credit Account
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/CreditLimitManager.sol#L80-L84

 function pauseCreditLimit(address user, address market) external onlyOwnerOrGuardian {
        require(IronBankInterface(ironBank).isCreditAccount(user), "cannot pause non-credit account");

        // Set the credit limit to a very small amount (1 Wei) to avoid the user becoming liquidatable.
        IronBankInterface(ironBank).setCreditLimit(user, market, 1);
    }

Setting Credit Limit
https://github.com/sherlock-audit/2023-05-ironbank/blob/main/ib-v2/src/protocol/pool/CreditLimitManager.sol#L80-L84

 function setCreditLimit(address user, address market, uint256 creditLimit) external onlyOwner {
        IronBankInterface(ironBank).setCreditLimit(user, market, creditLimit);
    }

Modifier onlyOwnerOrGuardian

 modifier onlyOwnerOrGuardian() {
        require(msg.sender == owner() || msg.sender == guardian, "!authorized");
        _;
    }

Tool used

Manual Review

Recommendation

  1. rename the modifer as onlyGuardian and change the logic to check against guardian as below
 modifier onlyGuardian() {
        require(msg.sender == guardian, "!authorized");
        _;
    }
  1. Assign the onlyGuardian() modifier to pauseCreditLimit() function.

  2. update the setCreditLimit function to check for account to be not CreditAccount, before setting the new creditLimit.

 function setCreditLimit(address user, address market, uint256 creditLimit) external onlyOwner {
        require(!IronBankInterface(ironBank).isCreditAccount(user), "cannot set for credit account");
        IronBankInterface(ironBank).setCreditLimit(user, market, creditLimit);
    }

Let guardian handle until it remains as creditAccount by locking the creditLimit to 1.

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.