Fast Steel Cormorant
Medium
The executeMintUnbacked
function within the BridgeLogic
contract of Aave v3.3 is vulnerable to a race condition that permits the minting of unbacked
tokens beyond the established unbackedMintCap
. This flaw arises from the sequence in which the reserve.unbacked
value is incremented before validating against the minting cap, allowing attackers to exploit simultaneous transactions to bypass intended restrictions.
The vulnerability is rooted in the improper ordering of state mutation and validation within the executeMintUnbacked
function. The critical code segments are as follows:
-
State Mutation Before Validation: The
reserve.unbacked
is incremented with theamount
prior to checking if this new value exceeds theunbackedMintCap
. This allows multiple transactions to pass the cap check based on the initial state before any increments from concurrent transactions are considered. -
Lack of Atomicity: Solidity's EVM executes transactions sequentially, but without proper atomic checks or reentrancy guards, rapid succession of transactions can still manipulate state in unintended ways, especially under high network load or in specialized environments.
- Consequences:
- Unbacked Token Inflation: Attackers can mint more
unbacked
tokens than the protocol allows, diluting the value of tokens and undermining collateral integrity. - Liquidity Drain: Excessively minted
unbacked
tokens, when backed, can lead to disproportionate issuance ofaTokens
, allowing attackers to withdraw more liquidity than intended. - Financial Instability: The protocol may incur significant losses due to the over-issuance of tokens, leading to potential insolvency or the need for emergency interventions.
- Unbacked Token Inflation: Attackers can mint more
-
Monitoring Network State:
- The attacker observes the state of
reserve.unbacked
, particularly as it approaches theunbackedMintCap
.
- The attacker observes the state of
-
Crafting Multiple Mint Transactions:
- The attacker prepares several minting transactions, each with an
amount
that individually does not exceed the cap but cumulatively does.
- The attacker prepares several minting transactions, each with an
-
Simultaneous Submission:
- Submit all crafted transactions in rapid succession, aiming for them to be mined within the same block or in quick succession where state mutations overlap.
-
Exploiting the Race Condition:
- Transaction 1 (Tx1):
- Increments
reserve.unbacked
from 950,000 to 980,000. - Passes the cap check (
980,000 <= 1,000,000
).
- Increments
- Transaction 2 (Tx2):
- Independently reads
reserve.unbacked
as 950,000 (before Tx1's mutation). - Increments to 990,000.
- Passes the cap check (
990,000 <= 1,000,000
).
- Independently reads
- Transaction 3 (Tx3):
- Independently reads
reserve.unbacked
as 950,000. - Increments to 970,000.
- Passes the cap check (
970,000 <= 1,000,000
).
- Independently reads
- Transaction 1 (Tx1):
-
Resulting State:
- After all transactions,
reserve.unbacked
is erroneously set to 1,070,000, surpassing theunbackedMintCap
of 1,000,000.
- After all transactions,
-
Backing Excess Tokens:
- The attacker calls
executeBackUnbacked
to back the inflatedunbacked
tokens, receiving moreaTokens
than justified, thereby draining protocol liquidity.
- The attacker calls
- Reordering Operations:
- Validation Before Mutation: Ensure that the cap check occurs before updating
reserve.unbacked
.function executeMintUnbacked( // parameters... ) external { DataTypes.ReserveData storage reserve = reservesData[asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); reserve.updateState(reserveCache); ValidationLogic.validateSupply(reserveCache, reserve, amount, onBehalfOf); uint256 unbackedMintCap = reserveCache.reserveConfiguration.getUnbackedMintCap(); uint256 reserveDecimals = reserveCache.reserveConfiguration.getDecimals(); + uint256 currentUnbacked = reserve.unbacked; + require( + currentUnbacked + amount <= unbackedMintCap * (10 ** reserveDecimals), + Errors.UNBACKED_MINT_CAP_EXCEEDED + ); + reserve.unbacked = currentUnbacked + amount.toUint128(); - uint256 unbacked = reserve.unbacked += amount.toUint128(); require( unbacked <= unbackedMintCap * (10 ** reserveDecimals), Errors.UNBACKED_MINT_CAP_EXCEEDED ); // Rest of the function... }
- Validation Before Mutation: Ensure that the cap check occurs before updating