Reach out for an audit or to learn more about Macro
or Message on Telegram

Mento A-1

Security Audit

August 17th, 2023

Version 1.0.0

Presented by 0xMacro

Table of Contents

Introduction

This document includes the results of the security audit for Mento's smart contract code as found in the section titled ‘Source Code’. The security audit was performed by the Macro security team from July 17, 2023 to August 2, 2023.

The purpose of this audit is to review the source code of certain Mento Solidity contracts, and provide feedback on the design, architecture, and quality of the source code with an emphasis on validating the correctness and security of the software in its entirety.

Disclaimer: While Macro’s review is comprehensive and has surfaced some changes that should be made to the source code, this audit should not solely be relied upon for security, as no single audit is guaranteed to catch all possible bugs.

Overall Assessment

The following is an aggregation of issues found by the Macro Audit team:

Severity Count Acknowledged Won't Do Addressed
High 1 - - 1
Medium 3 1 - 2
Low 3 - - 3
Code Quality 7 - - 7

Mento was quick to respond to these issues.

Specification

Our understanding of the specification was based on the following sources:

Source Code

The following source code was reviewed during the audit:

Specifically, we audited the following contracts within this repository:

Contract SHA256
contracts/interfaces/IBiPoolManager.sol

3c9be43f61b59320ef4b1adfb4cc3ecc729d07d7fa9a8cc56af924ea0e6c8e1c

contracts/interfaces/IBreaker.sol

09fbdc17b0e3eeb23ed0e8b256f4838a29e6a46363df4d95d89cdc4312263e18

contracts/interfaces/IBreakerBox.sol

9c916b36827f3eea1c9ac3d969c12398793e6abf68c2b16f05dca7d1b3843cb4

contracts/interfaces/IPricingModule.sol

d6bb235ed782868ea3872ba33b7dccdee0a453b7ca1f798a91fdb0b11e8f3ed8

contracts/interfaces/IStableTokenV2.sol

f3e8adfe9b07940a6fe9899b57a60f4d5f8325774a2fbf30b4befc5c211ce62b

contracts/oracles/BreakerBox.sol

73ae9fa34b7b5e132466ac25c533ef8b2c5d1d235f7ad088d2100abfc91e0c96

contracts/oracles/breakers/MedianDeltaBreaker.sol

ee5ad0d553a382b6943536ac75006b4161b272b0afd794b5ead4671aa29bfd5f

contracts/swap/BiPoolManager.sol

5199d9836a5e500e3ec690863c4495c16368c943529b14182ebf35ef5bd8adf3

contracts/swap/ConstantSumPricingModule.sol

cc6aa0fb52a6ae6f31ce16e46c4a9c2bf5852bcd89b908d231d5c3c162680f96

contracts/tokens/StableTokenV2.sol

74244e13bd8ba02b17826710fed12b4c881bf4b560ae25b02b5733d0ac0ca53d

contracts/tokens/patched/ERC20PermitUpgradeable.sol

439b890871561da7e35aa337f306e4b11f82d224fa01f33f6c7b54843333577b

contracts/tokens/patched/ERC20Upgradeable.sol

7840f75996155cbd18c4e2589a0c235266adaa15491324f793e4c9ab49d243ff

Note: This document contains an audit solely of the Solidity contracts listed above. Specifically, the audit pertains only to the contracts themselves, and does not pertain to any other programs or scripts, including deployment scripts.

Issue Descriptions and Recommendations

Click on an issue to jump to it, or scroll down to see them all.

Security Level Reference

We quantify issues in three parts:

  1. The high/medium/low/spec-breaking impact of the issue:
    • How bad things can get (for a vulnerability)
    • The significance of an improvement (for a code quality issue)
    • The amount of gas saved (for a gas optimization)
  2. The high/medium/low likelihood of the issue:
    • How likely is the issue to occur (for a vulnerability)
  3. The overall critical/high/medium/low severity of the issue.

This third part – the severity level – is a summary of how much consideration the client should give to fixing the issue. We assign severity according to the table of guidelines below:

Severity Description
(C-x)
Critical

We recommend the client must fix the issue, no matter what, because not fixing would mean significant funds/assets WILL be lost.

(H-x)
High

We recommend the client must address the issue, no matter what, because not fixing would be very bad, or some funds/assets will be lost, or the code’s behavior is against the provided spec.

(M-x)
Medium

We recommend the client to seriously consider fixing the issue, as the implications of not fixing the issue are severe enough to impact the project significantly, albiet not in an existential manner.

(L-x)
Low

The risk is small, unlikely, or may not relevant to the project in a meaningful way.

Whether or not the project wants to develop a fix is up to the goals and needs of the project.

(Q-x)
Code Quality

The issue identified does not pose any obvious risk, but fixing could improve overall code quality, on-chain composability, developer ergonomics, or even certain aspects of protocol design.

(I-x)
Informational

Warnings and things to keep in mind when operating the protocol. No immediate action required.

(G-x)
Gas Optimizations

The presented optimization suggestion would save an amount of gas significant enough, in our opinion, to be worth the development cost of implementing it.

Issue Details

H-1

MedianDeltaBreaker.shouldTrigger() allows any external actor to incorrectly update the medianRatesEMA

Topic
Specification
Status
Impact
Spec Breaking
Likelihood
Medium
Restricted access to shouldTrigger() function so only BreakerBox can call it.

The MedianDeltaBreaker contract implements an Exponential Moving Average (EMA) to calculate the percentage difference between the current reported median and the previous median rate EMA value, calculated from the following formula:

currentMedian * smoothingFactor + previousMedianEMA * (1 - smoothingFactor)

Where

  • smoothingFactor is set for each specific rateFeedID with setSmoothingFactor() and can’t be higher than 1.
  • currentMedian is the externally reported value from the SortedOracles.medianRate().

This allows the MedianDeltaBreaker to weigh the latest median provided by oracles (reported through SortedOracles.report() and read through SortedOracles.medianRate()) and consider the previously stored value.

function shouldTrigger(address rateFeedID) public returns (bool triggerBreaker) {
  (uint256 currentMedian, ) = sortedOracles.medianRate(rateFeedID);

  uint256 previousRatesEMA = medianRatesEMA[rateFeedID];
  ...
  FixidityLib.Fraction memory smoothingFactor = FixidityLib.wrap(getSmoothingFactor(rateFeedID));
  medianRatesEMA[rateFeedID] = FixidityLib
    .wrap(currentMedian)
    .multiply(smoothingFactor)
    .add(FixidityLib.wrap(previousRatesEMA).multiply(FixidityLib.fixed1().subtract(smoothingFactor)))
    .unwrap(); 

  return exceedsThreshold(previousRatesEMA, currentMedian, rateFeedID);
}

However, this implementation will only remain true if shouldTrigger() function is called once per value update in the SortedOracle median rate; and since the shouldTrigger() function is public (as per the IBreaker.sol interface) and doesn’t have any access control, any external actor can perform any arbitrary shouldTrigger() calls incorrectly updating the medianRatesEMA values with the same reported medianRate.

Remediations to Consider

  • Updating the medianRatesEMA only if a new report was provided for that specific rateFeedID or
  • Adding access control to this function and allowing only to call it by SortedOracles or BreakerBox contracts.

Note: For the second suggestion, it's worth adding an additional view function to allow users or external points to check this condition.

M-1

setTokenPrecisionMultipliers allows arbitrary decimal multipliers

Topic
Input Validation
Status
Impact
High
Likelihood
Low
Removed the function since precision multipliers are automatically set in createExchange() and are only used for exchanges.

When the BiPoolManager creates an exchange, after performing several validations on the inputs, the token precision multipliers are set in order to deal with assets with different token decimals while calculating swap amounts.

For example, USDC has 6 decimal places, having a token precision multiplier of 1e12 (10 ** (18 - 6)), as we can see in the function’s logic:

tokenPrecisionMultipliers[exchange.asset0] = 10**(18 - uint256(IERC20Metadata(exchange.asset0).decimals()));
tokenPrecisionMultipliers[exchange.asset1] = 10**(18 - uint256(IERC20Metadata(exchange.asset1).decimals()));

However, the function setTokenPrecisionMultipliers allows to arbitrarily change the token precision multipliers, in this case, say for USDC, from 1e12 to an arbitrarily large or small value. Which would then affect the calculations in the swap in/out and in the get amounts in/out functions.

Additionally, the function doesn’t check whether the tokens are valid and registered with the Reserve contract.

Remediations to Consider

  • Perform the same proper input validations implemented in the createExchange function or
  • Remove the setTokenPrecisionMultipliers from the contract.
M-2

Pricing module is not verified when creating exchanges

Topic
Input Validation
Status
Impact
High
Likelihood
Low
Introduced a new pricingModules mapping that tracks the current pricingModule implementations.

When creating an exchange in the BiPoolManager contract, the pricing module is specified as an input parameter in the PoolExchange _exchange struct. However, there are no validations that the address of the pricing module corresponds to a canonical version of this module. This can cause, for example, the previous version of the constant sum module to be used in the exchange creation and, therefore, to calculate swap amounts incorrectly. The only check performed during swaps is in the pricingModuleIdentifier:

function pricingModuleIdentifier(PoolExchange memory exchange) internal view returns (bytes32) {
  return keccak256(abi.encodePacked(exchange.pricingModule.name()));
} 

This also opens up other scenarios where invalid pricing module addresses can be used.

Remediations to Consider

Keeping in the BiPoolManager contract’s storage the address of the latest or allowed pricing modules and asserting it’s used in createExchange.

M-3

MedianDeltaBreaker will store the first median reference without restrictions

Topic
Protocol Design
Status
Acknowledged
Impact
High
Likelihood
Low

The MedianDeltaBreaker contract implements an EMA (Exponential Moving Average) that weights the current median rate and adds as a consideration the previously stored median EMA rate; this mitigates immediate price spikes in the median but also allows constant trends in the price to move without triggering the breaker.

As we can see in the shouldTrigger() logic, there is a special consideration when the previousRatesEMA == 0. This condition will only be true in the initial call after enabling this breaker for a specific rateFeedID.

function shouldTrigger(address rateFeedID) public returns (bool triggerBreaker) {
    ...
  uint256 previousRatesEMA = medianRatesEMA[rateFeedID];
  if (previousRatesEMA == 0) {
    // Previous recorded EMA will be 0 the first time this rate feed is checked.
    medianRatesEMA[rateFeedID] = currentMedian;
    return false;
  }
    ...
}

However, since this function will store the current median without any constraints, the reported median, at the execution time of shouldTrigger() call, could be outside of the expected values or manipulated. This will render an invalid median that will be used as a reference to check every subsequent oracle report for that rateFeedID.

Remediations to Consider

  • Adding an expected range of median values where the initial medianRatesEMA value will be stored and used as reference, or
  • Setting an initial reference median value, similar to the ValueDeltaBreaker contract.
Response by Mento

We don’t believe the situation described is a credible threat, the assumption is that we have an attacker that can manipulate an Oracle price and set the initial EMA value when a circuit breaker gets added or activated via Governance proposal. In this situation, it wouldn’t make sense for the attacker to wait until that moment to manipulate the price.

And even so, there’s no clear way to seed the value that makes sense. If we provide an initial value, it will already be old until the governance proposal passes to the execution state so we would be seeding an old value. A range would add a lot of configuration complexity without providing many benefits and could result in problematic behavior if the range is too tight or too wide.

L-1

Potential misconfiguration in trading limits

Topic
Input Validation
Status
Impact
Low
Likelihood
Low
Added additional checks for trading limits.

The trading limits act as a safeguard and are checked during the swap operation if the trading config was created for the token. A valid configuration should not permit setting these limits to 0 for enabled directional trading flags.

The function Broker.configureTradingLimit() doesn’t validate the above criteria when setting the trading config for a token.

In addition, there are no checks to ensure that the trading limit for L0 is smaller than the trading limit for L1, and they’re both smaller than the global trading limit LG.

Remediations to Consider

Consider validating the trading config inputs to ensure that invalid trading limits cannot be set.

L-2

toggleBreaker does not update the trading mode for newly added breakers

Topic
Edge Case
Status
Impact
Medium
Likelihood
Low
Added a call to checkAndSetBreakers() intoggleBreaker()

In BreakerBox.sol, when turning on the breakers for certain rateFeedIds, there will be an execution gap between the toggleBreaker() call and the comparison of the corresponding breaker trigger conditions in checkAndSetBreakers().

Since the BiPoolManager contract uses the view function getRateFeedTradingMode() before executing swaps, which reads the current breaker status but doesn’t update each dependency:

function getRateFeedTradingMode(address rateFeedID) external view returns (uint8) {
require(rateFeedStatus[rateFeedID], "Rate feed ID has not been added");
uint8 tradingMode = rateFeedTradingMode[rateFeedID];
for (uint256 i = 0; i < rateFeedDependencies[rateFeedID].length; i++) {
  tradingMode = tradingMode | rateFeedTradingMode[rateFeedDependencies[rateFeedID][i]];
}
return tradingMode;
}

It is possible that the conditions that trigger a breaker may already be present. However, until the next oracle feed SortedOracles::report(), or external call to checkAndSetBreakers(), the trading mode won’t consider this breaker. This is especially visible with the MedianDeltaBreaker, where the first update will only store the median rate.

Remediations to Consider

  • Calling checkAndSetBreakers when adding breakers to a specific rateFeedID in the toggleBreaker logic or,
  • Make sure to execute a checkAndSetBreakers atomically call after enabling a breaker.
L-3

MedianDeltaBreaker previous rates can not be reset if needed

Topic
Interoperability
Status
Impact
Low
Likelihood
Low
Introduced a new protected resetMedianRateEMA() function.

In MedianDeltaBreaker, the initial value and start point, where medianRatesEMA == 0, only exist the first time a specific rateFeedID is set for this breaker. When a rateFeedID needs to be removed and later added again into the MedianDeltaBreaker, there is no way to reset the initial reference value, which means it cannot easily adjust to newly established rates. It will require a custom higher configuration of the breaker threshold to temporarily bypass the reported values.

Remediations to Consider

Implementing a reset function if the breaker is disabled from the BreakerBox contract to allow a new initialization of the median rates.

Q-1

Repetitive validation code can be converted to a function modifier

Topic
Duplicated Code
Status
Quality Impact
Low

The following require statement is used in the internal functions_getAmountIn and _getAmountOut contained in the BiPoolManager.sol. This is repeated code that could be refactored into a function modifier.

//The following require statement is used in multiple functions 
require(
      (tokenIn == exchange.asset0 && tokenOut == exchange.asset1) ||
        (tokenIn == exchange.asset1 && tokenOut == exchange.asset0),
      "tokenIn and tokenOut must match exchange"
    );

Remediations to Consider

Consider converting this validation code to a function modifier and using it for the internal functions _getAmountOut and _getAmountIn.

Q-2

The function removeRateFeed() doesn’t delete the rate feed dependencies

Topic
Data Model
Status
Quality Impact
Medium

In the BreakerBox contract, a rateFeedId could have a set of rate feed dependencies mapped to it via the setRateFeedDependencies function.

However, when a rate feed is removed, its dependencies are not deleted.

Remediations to Consider

Consider adding the following line to the removeRateFeed function

delete rateFeedDependencies[rateFeedID];
Q-3

Missing natspec comments for struct state and struct config in the TradingLimits library

Topic
Natspec
Status
Quality Impact
Low

Consider adding the corresponding natspec comments to these structs.

Q-4

Documentation improvements

Topic
Documentation
Status
Addressed
Quality Impact
Medium
  1. In the BiPoolManager docs page, the Executing Swaps section mentions the following:

    This function executes, in that it prices the swap and updates the virtual bucket sizes of the PoolExchange.

    However, it doesn’t consider the case where an exchange uses the ConstantSumPricingModule, which doesn’t update the bucket sizes on swaps.

  2. On the Pricing Modules page, both pricing module links to GitHub are broken. Also, the constant sum pricing module formula should be updated to reflect the newly implemented value constant sum formula.

  3. Some of the links included in the documentation pages are currently broken:

  4. Consider adding documentation pages for the specific breaker contract implementations (MedianDeltaBreaker and ValueDeltaBreaker).

Q-5

createExchange performs the same external call twice

Topic
Duplicated Call
Status
Quality Impact
Medium

In BiPoolManager.sol, when asserting the asset0 and asset1 decimals and setting the tokenPrecisionMultipliers, createExchange() function performs the same external call twice for each operation.

if (IERC20Metadata(exchange.asset0).decimals() > 18) {
  revert("asset0 decimals must be <= 18");
}
if (IERC20Metadata(exchange.asset1).decimals() > 18) {
  revert("asset1 decimals must be <= 18");
}

tokenPrecisionMultipliers[exchange.asset0] = 10**(18 - uint256(IERC20Metadata(exchange.asset0).decimals()));
tokenPrecisionMultipliers[exchange.asset1] = 10**(18 - uint256(IERC20Metadata(exchange.asset1).decimals()));

Consider saving these values to memory to save gas and avoid unnecessary external calls.

Q-6

Use returned values for declarations in getOracleExchangeRate()

Topic
Code Quality
Status
Quality Impact
Low

Consider using the return values in the function declaration to improve readability and consistency with other functions.

function getOracleExchangeRate(address target) internal view returns (uint256 rateNumerator, uint256 rateDenominator) {
    (rateNumerator, rateDenominator) = sortedOracles.medianRate(target);
    require(rateDenominator > 0, "exchange rate denominator must be greater than 0");
  }
Q-7

StableTokenV2 storage __gap uses existent storage slots

Topic
Data Model
Status
Quality Impact
High

In StableTokenV2, the storage layout implemented deprecated variables for unused slots from the previous StableToken implementation. However, for slots 8 and 12, inflationState and exchangeRegistryId respectively, there aren’t any deprecated variables declarations. These slots are included in the __gap reserve variable.

Consider adding deprecated variables for these slots to keep track of these unused but non-empty variables in case of any future implementation migration.

Disclaimer

Macro makes no warranties, either express, implied, statutory, or otherwise, with respect to the services or deliverables provided in this report, and Macro specifically disclaims all implied warranties of merchantability, fitness for a particular purpose, noninfringement and those arising from a course of dealing, usage or trade with respect thereto, and all such warranties are hereby excluded to the fullest extent permitted by law.

Macro will not be liable for any lost profits, business, contracts, revenue, goodwill, production, anticipated savings, loss of data, or costs of procurement of substitute goods or services or for any claim or demand by any other party. In no event will Macro be liable for consequential, incidental, special, indirect, or exemplary damages arising out of this agreement or any work statement, however caused and (to the fullest extent permitted by law) under any theory of liability (including negligence), even if Macro has been advised of the possibility of such damages.

The scope of this report and review is limited to a review of only the code presented by the Mento team and only the source code Macro notes as being within the scope of Macro’s review within this report. This report does not include an audit of the deployment scripts used to deploy the Solidity contracts in the repository corresponding to this audit. Specifically, for the avoidance of doubt, this report does not constitute investment advice, is not intended to be relied upon as investment advice, is not an endorsement of this project or team, and it is not a guarantee as to the absolute security of the project. In this report you may through hypertext or other computer links, gain access to websites operated by persons other than Macro. Such hyperlinks are provided for your reference and convenience only, and are the exclusive responsibility of such websites’ owners. You agree that Macro is not responsible for the content or operation of such websites, and that Macro shall have no liability to your or any other person or entity for the use of third party websites. Macro assumes no responsibility for the use of third party software and shall have no liability whatsoever to any person or entity for the accuracy or completeness of any outcome generated by such software.