<!-- canonical: https://0xsimao.com/findings/mento-frontrun-mint-ubi-stuck -->

# Malicious user may frontrun `GoodDollarExpansionController::mintUBIFromReserveBalance()` to make protocol funds stuck

Medium · Sherlock · Stablecoin · 24th October 2024

Finding M-3 of the Mento competition.

- Protocol: https://audits.sherlock.xyz/contests/598
- Report: /reports/mento
- Codebase: https://github.com/0xsimao/2024-10-mento-update/tree/098b17fb32d294145a7f000d96917d13db8756cc
- Source: https://github.com/sherlock-audit/2024-10-mento-update-judging/issues/33

---

### Summary

[GoodDollarExpansionController::mintUBIFromReserveBalance()](https://github.com/sherlock-audit/2024-10-mento-update/blob/main/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol#L153) or [GoodDollarExpansionController::mintUBIFromInterest()](https://github.com/sherlock-audit/2024-10-mento-update/blob/main/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol#L137) transfer funds to the reserve and mint $G to the distribution helper. However,  [GoodDollarExchangeProvider::mintFromInterest()](https://github.com/sherlock-audit/2024-10-mento-update/blob/main/mento-core/contracts/goodDollar/GoodDollarExpansionController.sol#L142) [mints 0 tokens](https://github.com/sherlock-audit/2024-10-mento-update/blob/main/mento-core/contracts/goodDollar/GoodDollarExchangeProvider.sol#L179-L181) whenever the supply is 0. An attacker can buy all $G from the exchange to trigger this.

### Root Cause

In `GoodDollarExpansionController::142` and `GoodDollarExpansionController::161`, `amountMinted` is not checked for a null value.

### Internal pre-conditions

None.

### External pre-conditions

None.

### Attack Path

1. Attacker calls `Bancor::swapIn()` or `Bancor::swapOut()`, buying all $G in the exchange, making `PoolExchange.tokenSupply` null.
2. `GoodDollarExpansionController::mintUBIFromReserveBalance()` or `GoodDollarExpansionController::mintUBIFromInterest()` is called, adding reserve asset funds without minting $G.

### Impact

Funds are added to the reserve without the corresponding amount of $G being minted.

### PoC

`GoodDollarExpansionController::mintUBIFromInterest()` and `GoodDollarExpansionController::mintUBIFromReserveBalance()` do not check if `amountToMint` is null:
```solidity
function mintUBIFromInterest(bytes32 exchangeId, uint256 reserveInterest) external {
  require(reserveInterest > 0, "Reserve interest must be greater than 0");
  IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider))
    .getPoolExchange(exchangeId);

  uint256 amountToMint = goodDollarExchangeProvider.mintFromInterest(exchangeId, reserveInterest);

  require(IERC20(exchange.reserveAsset).transferFrom(msg.sender, reserve, reserveInterest), "Transfer failed"); //@audit safeTransferFrom.  //@audit lost if reserve asset is also a stable asset
  IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountToMint);

  // Ignored, because contracts only interacts with trusted contracts and tokens
  // slither-disable-next-line reentrancy-events
  emit InterestUBIMinted(exchangeId, amountToMint);
}

...

function mintUBIFromReserveBalance(bytes32 exchangeId) external returns (uint256 amountMinted) {
  IBancorExchangeProvider.PoolExchange memory exchange = IBancorExchangeProvider(address(goodDollarExchangeProvider))
    .getPoolExchange(exchangeId);

  uint256 contractReserveBalance = IERC20(exchange.reserveAsset).balanceOf(reserve);
  uint256 additionalReserveBalance = contractReserveBalance - exchange.reserveBalance;
  if (additionalReserveBalance > 0) {
    amountMinted = goodDollarExchangeProvider.mintFromInterest(exchangeId, additionalReserveBalance);
    IGoodDollar(exchange.tokenAddress).mint(address(distributionHelper), amountMinted);

    // Ignored, because contracts only interacts with trusted contracts and tokens
    // slither-disable-next-line reentrancy-events
    emit InterestUBIMinted(exchangeId, amountMinted);
  }
}
```

`GoodDollarExchangeProvider::mintFromInterest()` returns 0 if `exchange.tokenSupply` is 0.
```solidity
function mintFromInterest(
  bytes32 exchangeId,
  uint256 reserveInterest
) external onlyExpansionController whenNotPaused returns (uint256 amountToMint) {
  PoolExchange memory exchange = getPoolExchange(exchangeId);

  uint256 reserveinterestScaled = reserveInterest * tokenPrecisionMultipliers[exchange.reserveAsset];
  uint256 amountToMintScaled = unwrap(
    wrap(reserveinterestScaled).mul(wrap(exchange.tokenSupply)).div(wrap(exchange.reserveBalance))
  );
  amountToMint = amountToMintScaled / tokenPrecisionMultipliers[exchange.tokenAddress];

  exchanges[exchangeId].tokenSupply += amountToMintScaled;
  exchanges[exchangeId].reserveBalance += reserveinterestScaled;

  return amountToMint;
}
```

### Mitigation

Revert if the `amountToMint` from the `GoodDollarExchangeProvider::mintFromInterest()` call is null. The same should also be done for `GoodDollarExpansionController::mintUBIFromExpansion()` `amountMinted` from the `GoodDollarExchangeProvider.mintFromExpansion()` call.

---

Related findings:

- [`BoldToken` initialization can be frontrun to silently mint/approve bold to an attacker](https://0xsimao.com/findings/nerite-initialization-frontrun-mint-approve): Nerite
- [Drained lender due to `LenderCommitmentGroup_Smart::acceptFundsForAcceptBid()` `_collateralAmount` by `STANDARD_EXPANSION_FACTOR` multiplication](https://0xsimao.com/findings/teller-finance-bid-collateral-factor-multiplication): Teller Finance
- [A malicious user may unlock instantly all the funds from the `FluidLocker` when no one is staking in the Tax pool](https://0xsimao.com/findings/superfluid-locker-system-unlock-instantly-staking-tax): Superfluid Locker System
- [DoSed liquidations as `PrizeVault::liquidatableBalanceOf()` does not take into account the `mintLimit` when the token out is the asset](https://0xsimao.com/findings/pooltogether-the-prize-layer-for-defi-liquidations-prize-liquidatable-mint): PoolTogether Prize Layer
