HardhatDeFi
15,000 USDC
View results
Submission Details
Severity: high
Invalid

Invariant Violation: Direct wToken Burning in `AaveDIVAWrapperCore._redeemWToken` Breaks Equality with Position Token Supply

Summary

The AaveDIVAWrapperCore contract's internal function _redeemWToken allows users to directly burn wTokens in exchange for underlying collateral from Aave, without correspondingly burning the associated long/short position tokens in the DIVA Protocol pool. This function can be called by the external functions redeemWToken and batchRedeemWToken in the AaveDIVAWrapper contract. Since users can obtain wTokens not only through pool creation and liquidity addition but also via DIVA Protocol's transferFeeClaim and claimFee functions, this vulnerability breaks the documented invariant: short token supply = long token supply = wToken supply. The root cause is that _redeemWToken interacts solely with wTokens and Aave, bypassing DIVA Protocol's accounting, which is responsible for maintaining the balance between wTokens and position tokens. Consequently, this allows users to redeem more collateral than entitled, potentially leading to a loss of funds for other users.

Vulnerability Details

The core issue lies in the _redeemWToken function within the AaveDIVAWrapperCore contract (AaveDIVAWrapperCore.sol#L314-L330). This function is designed to allow users to redeem their wTokens for the underlying collateral assets held within the Aave protocol, by calling the function _redeemWTokenPrivate. However, it fails to interact with the corresponding DIVA Protocol pool to adjust the balances of the associated long and short position tokens:

function _redeemWToken(address _wToken, uint256 _wTokenAmount, address _recipient) internal returns (uint256) {
// Note: wTokens are not transferred to this contract. Instead, they are burnt from the caller's balance by this contract,
// which has the authority to do so as the owner of the wToken. Therefore, no prior approval from the caller is needed.
// Use the user's balance if `_wTokenAmount` equals `type(uint256).max`
uint256 _userBalance = IERC20Metadata(_wToken).balanceOf(msg.sender);
uint256 _wTokenAmountToRedeem = _wTokenAmount;
if (_wTokenAmount == type(uint256).max) {
_wTokenAmountToRedeem = _userBalance;
}
// Withdraw collateral token from Aave, burn wTokens and transfer collateral token to `_recipient`.
// Reverts inside the wToken's burn function if the `_wTokenAmountToRedeem` exceeds the user's wToken balance.
@> uint256 _amountReturned = _redeemWTokenPrivate(_wToken, _wTokenAmountToRedeem, _recipient, msg.sender);
return _amountReturned;
}
function _redeemWTokenPrivate(
address _wToken,
uint256 _wTokenAmount,
address _recipient,
address _burnFrom
) private returns (uint256) {
if (_recipient == address(0)) revert ZeroAddress();
// Burn the specified amount of wTokens. Only this contract has the authority to do so.
// Reverts if `_wTokenAmount` exceeds the user's wToken balance.
@> IWToken(_wToken).burn(_burnFrom, _wTokenAmount);
address _collateralToken = _wTokenToCollateralToken[_wToken];
// Withdraw the collateral asset from Aave, which burns the equivalent amount of aTokens owned by this contract.
// E.g. User has 100 aUSDC, calls withdraw() and receives 100 USDC, burning the 100 aUSDC.
// Collateral token is transferred to `_recipient`.
// Reverts if the collateral token is not a registered wToken (first parameter will be address(0)).
@> uint256 _amountReturned = IAave(_aaveV3Pool).withdraw(
_collateralToken, // Address of the underlying asset (e.g., USDT), not the aToken.
_wTokenAmount, // Amount to withdraw.
_recipient // Address that will receive the underlying asset.
);
emit WTokenRedeemed(_wToken, _wTokenAmount, _collateralToken, _amountReturned, _recipient);
return _amountReturned;
}

This violates the invariant short token supply = long token supply = wToken supply should be maintained as to the AaveDIVAWrapper - Documentation:

## Invariants
At any point in time, the following invariants have to hold true:
- `aToken balance of AaveDIVAWrapper >= wToken supply`
- `short token supply = long token supply = wToken supply`

In contrast, the functions _removeLiquidity and _redeemPositionToken within the AaveDIVAWrapperCore contract, which also involve wToken redemption, correctly interact with the DIVA Protocol to burn the corresponding position tokens thus maintain the protocol invariant:

function _removeLiquidity(
bytes32 _poolId,
uint256 _positionTokenAmount,
address _recipient
) internal returns (uint256) {
// ...
// Remove liquidity on DIVA Protocol to receive wTokens, and calculate the returned wToken amount (net of DIVA fees)
// as DIVA Protocol's removeLiquidity function does not return the amount of collateral token received.
uint256 _wTokenBalanceBeforeRemoveLiquidity = _collateralTokenContract.balanceOf(address(this));
@> IDIVA(_diva).removeLiquidity(_poolId, _positionTokenAmountToRemove);
uint256 _wTokenAmountReturned = _collateralTokenContract.balanceOf(address(this)) -
_wTokenBalanceBeforeRemoveLiquidity;
// ...
// Withdraw collateral token from Aave, burn wTokens owned by this contract and transfer collateral token to `_recipient`.
@> uint256 _amountReturned = _redeemWTokenPrivate(
_pool.collateralToken, // wToken
_wTokenAmountReturned,
_recipient,
address(this)
);
// ...
}
/**
* @notice Function to remove collateral from an existing pool.
@> * @dev Requires `msg.sender` to return an equal amount of long and short
@> * position tokens which are burnt. Collateral amount returned to the user
* is net of fees. Protocol and settlement fees for DIVA treasury and
* data provider, respectively, are retained within the contract and can
* be claimed via `claimFee` function.
* @param _poolId Id of the pool that a user wants to remove collateral
* from.
* @param _amount Number of position tokens to return (1:1 to collateral
* amount).
*/
function removeLiquidity(bytes32 _poolId, uint256 _amount) external;
function _redeemPositionToken(
address _positionToken,
uint256 _positionTokenAmount,
address _recipient
) internal returns (uint256) {
// ...
// Redeem position token on DIVA Protocol to receive wTokens, and calculate the returned wToken amount (net of DIVA fees)
// as DIVA Protocol's redeemPositionToken function does not return the amount of collateral token received.
uint256 _wTokenBalanceBeforeRedeem = _collateralTokenContract.balanceOf(address(this));
@> IDIVA(_diva).redeemPositionToken(_positionToken, _positionTokenAmountToRedeem);
uint256 _wTokenAmountReturned = _collateralTokenContract.balanceOf(address(this)) - _wTokenBalanceBeforeRedeem;
// ...
// Withdraw collateral token from Aave, burn wTokens owned by this contract and transfer collateral token to `_recipient`.
@> uint256 _amountReturned = _redeemWTokenPrivate(
_pool.collateralToken, // wToken
_wTokenAmountReturned,
_recipient,
address(this)
);
// ...
}
/**
@> * @notice Function to redeem position tokens. Position tokens are burnt
@> * during that process.
* @dev If the submission period expired without a challenge or a review
* period expired without another input from the data provider, the
* previously submitted final value is confirmed inside the function at
* first user redemption.
* @param _positionToken address of the position token to be redeemed.
* @param _amount number of position tokens to be redeemed..
*/
function redeemPositionToken(address _positionToken, uint256 _amount) external;

Meanwhile, the wToken can be obtained by interacting with DIVA protocol's existing functions transferFeeClaim, batchTransferFeeClaim and claimFee in addition to pool creation and liquidity addition, according to the interface description IDIVA.sol#L100-L125. When these functions are called, the received wTokens haven't been burnt thus the corresponding position tokens are not burnt as well:

/**
* @notice Function to transfer fee claim from entitled address
* to another address
@> * @param _recipient Address of fee claim recipient
@> * @param _collateralToken Collateral token address
* @param _amount Amount (expressed as an integer with collateral token
* decimals) to transfer to recipient
*/
function transferFeeClaim(address _recipient, address _collateralToken, uint256 _amount) external;
/**
* @notice Batch version of `transferFeeClaim`
@> * @param _argsBatchTransferFeeClaim List containing collateral tokens,
@> * recipient addresses and amounts (expressed as an integer with collateral
* token decimals)
*/
function batchTransferFeeClaim(ArgsBatchTransferFeeClaim[] calldata _argsBatchTransferFeeClaim) external;
/**
* @notice Function to claim allocated fee
* @dev List of collateral token addresses has to be obtained off-chain
* (e.g., from TheGraph)
@> * @param _collateralToken Collateral token address
@> * @param _recipient Fee recipient address
*/
function claimFee(address _collateralToken, address _recipient) external;

By allowing direct burning of those claimed wTokens without corresponding position token adjustments, the _redeemWToken function introduces an imbalance: A user can obtain wTokens, redeem them for collateral through this vulnerable function, and still retain their position tokens in the DIVA Protocol pool. This effectively allows them to extract collateral without reducing their position, breaking the intended economic model and potentially leading to a situation where the remaining position tokens are under-collateralized. The vulnerability can be triggered by the external functions redeemWToken (AaveDIVAWrapper.sol#L71-L77) and batchRedeemWToken (AaveDIVAWrapper.sol#L172-L187) in the AaveDIVAWrapper contract.

Impact

The vulnerability has a significant impact, directly breaking the core invariant of the AaveDIVAWrapper and DIVA Protocol integration, which states that short token supply = long token supply = wToken supply. This leads to the following consequences:

  1. Direct Financial Loss: Attackers can exploit this vulnerability to withdraw more collateral from Aave than they are entitled to. By obtaining wTokens through mechanisms like claimFee or transferFeeClaim in DIVA Protocol, and then using _redeemWToken to redeem them, they effectively extract value without relinquishing their corresponding position in the DIVA pool. This creates a scenario where the remaining position token holders are left with under-collateralized positions, facing potential losses.

  2. Economic Imbalance: The broken invariant destabilizes the economic model of the DIVA Protocol pools. The intended 1:1 backing of position tokens with wTokens is violated, leading to a misrepresentation of the actual collateralization within the system.

  3. Unfair Advantage: Attackers gain an unfair advantage by being able to redeem collateral without surrendering their position tokens. This undermines the fairness and integrity of the DIVA Protocol pools.

  4. Loss of User Trust: Such a fundamental flaw can severely damage user trust in both the AaveDIVAWrapper and the DIVA Protocol, leading to a loss of confidence and potential withdrawal of funds from the system.

  5. Potential for Systemic Risk: Depending on the scale of the exploit and the amount of funds locked in the system, this vulnerability could pose a systemic risk to the affected pools and potentially impact the broader DeFi ecosystem if not addressed promptly.

In summary, this vulnerability allows for direct financial exploitation, undermines the core economic model, creates an unfair advantage for attackers, and can lead to a loss of user trust and potential systemic risks. Therefore, it should be classified as a high-severity issue.

Tool Used

Manual Review

Recommendations

1. Remove the _redeemWToken function and its associated external functions:

  • Eliminate the _redeemWToken function from the AaveDIVAWrapperCore contract entirely.

  • Remove the user-facing functions redeemWToken and batchRedeemWToken from the AaveDIVAWrapper contract which call _redeemWToken internally.

This is the simplest and most secure solution. It forces users to interact with DIVA Protocol directly when they want to redeem their wTokens which ensures that the corresponding position tokens are burned, maintaining the crucial invariant.

2. Alternative (Future Consideration): Integrate Position Token Burning into _redeemWToken (Requires Changes to DIVA Protocol):

  • Introduce a new function in DIVA Protocol: This function, for example, burnPositionTokens(bytes32 poolId, address positionToken, uint256 amount), would allow the AaveDIVAWrapper contract (and potentially other whitelisted addresses) to burn position tokens directly, without receiving collateral in return.

  • Modify _redeemWToken in AaveDIVAWrapperCore:

    • Add a poolId parameter to identify the relevant DIVA pool.

    • Calculate the amount of long and short tokens to burn, based on the _wTokenAmount being redeemed.

    • Call the new burnPositionTokens function in DIVA Protocol before burning wTokens and withdrawing from Aave.

  • Consider a Whitelist for burnPositionTokens: To enhance security, limit the ability to call the new burnPositionTokens function to only whitelisted addresses (e.g., the AaveDIVAWrapper contract).

Updates

Lead Judging Commences

bube Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.