Skip to content

Latest commit

 

History

History
66 lines (45 loc) · 4.3 KB

File metadata and controls

66 lines (45 loc) · 4.3 KB

Attractive Latte Cricket

Medium

Unchecked Approval Reset Leading to Double Spend Attack in initialize() Function

Summary

In the initialize function, the contract approves the fiat token for use in Compound V3 without resetting or validating previous approvals. This unchecked approval can lead to potential double-spending attacks if an attacker manipulates approvals externally before the contract re-initializes.

Root Cause

The vulnerability stems from the initialize() function found at CompoundV3FiatReserve.sol#L33. In this function, the fiat.approve(address(compound)) call is made without resetting or verifying the current approval status. This lack of proper handling for token allowances can lead to reinitialization issues, allowing double approvals or manipulations if the contract state is tampered with.

Internal pre-conditions

  1. Admin (or an authorized user) needs to call initialize() function to set the fiat.approve(address(compound)) allowance without resetting the previous allowance to a new value.
  2. The compound contract address must remain the same between approvals, with no changes that would cause invalidation of prior allowances.
  3. There needs to be prior allowance set for fiat that could potentially be manipulated if not reset before calling initialize().
  4. The contract state must be initialized at least once (i.e., initializer(2) allows only one-time initialization), allowing the call to happen under the assumption the previous state is valid and unchanged.

External pre-conditions

  1. The Compound V3 market contract (external protocol) needs to remain the same after the fiat.approve() call in the initialize() function, meaning no changes occur in the contract address between transactions.
  2. The fiat token (external asset) must retain its previous allowance for the compound contract, so the unreset allowance can be utilized.
  3. The Compound V3 contract needs to continue operating without introducing a method or requirement for resetting allowances or providing security against excessive approvals.
  4. The Compound market or fiat token’s balance conditions must stay unchanged, allowing the existing approval setup to persist without immediate interference.

Attack Path

  1. The attacker obtains a role that interacts with the contract.
  2. The attacker calls the initialize() function after having manipulated the approval externally.
  3. This allows the attacker to increase their approved token balance and withdraw funds unjustly.

Impact

The issue can result in a double-spend scenario where the attacker gains an unfair advantage by modifying approval levels before the contract re-initializes. This creates a risk of unauthorized transfers and loss of funds for the contract and its users. Given that this contract manages fiat tokens through Compound V3, such a vulnerability could lead to significant financial losses.

PoC

  1. The attacker gains access to a contract interaction and manipulates the approval for the fiat token externally.
  2. The contract re-initializes by calling the initialize() function, which does not validate prior approvals, allowing the attacker to exploit the modified approval.
  3. This leads to an unauthorized increase in approved tokens, enabling the attacker to double spend or withdraw more tokens than intended.
function initialize() public virtual initializer(2) {
    __ReserveBase__initialize();

    // No check for previous approval values, leading to potential double spend exploit
    fiat.approve(address(compound));  
}

In this situation, the contract does not clear or verify previous approval settings, leaving room for manipulation.

Mitigation

To prevent this vulnerability, reset previous approvals to zero before re-initializing them. This will ensure that the contract is secure against potential double spend attacks.

function initialize() public virtual initializer(2) {
    __ReserveBase__initialize();

    // Reset previous approval before setting a new approval value
    fiat.approve(address(compound), 0);
    fiat.approve(address(compound), type(uint256).max);  // Set a specific maximum approval value
}