Summary
The AaveDIVAWrapper contract includes a function called addLiquidity, which allows users to add _collateralAmount of liquidity to an existing DIVA Protocol pool identified by the provided _poolId. As part of this process, long position tokens are sent to _longRecipient, while short position tokens are sent to _shortRecipient. Additionally, users can remove liquidity using the removeLiquidity function, which removes liquidity from the specified _poolId by burning an equal amount (_positionTokenAmount) of valid long and short tokens and subsequently transferring the corresponding collateral tokens (e.g., USDC) to the designated _recipient.
To achieve this, AaveDIVAWrapper::addLiquidity internally calls AaveDIVAWrapperCore::_addLiquidity, and AaveDIVAWrapper::removeLiquidity calls AaveDIVAWrapperCore::_removeLiquidity. However, a chronological bug exists between these two functions, potentially leading to a Denial of Service (DoS) for liquidity providers.
In the AaveDIVAWrapperCore::_addLiquidity function, the msg.sender (liquidity provider) has the ability to specify recipients for the position tokens long tokens are sent to _longRecipient, and short tokens to _shortRecipient. While msg.sender initially provides liquidity, they lose ownership of the position tokens upon distribution, as there is no mechanism ensuring that msg.sender retains any reclaimable rights or approvals over them. Consequently, when msg.sender attempts to call removeLiquidity, the transaction fails due to a lack of ownership of the required position tokens, since the recipients (long and short token holders) must approve the transfer or transfer the position tokens back to msg.sender but may refuse to do so.
This vulnerability can effectively lock liquidity in the protocol, preventing providers from reclaiming their collateral and leading to a Denial of Service (DoS) scenario where liquidity providers are unable to exit their positions. Only recipients of the respective position tokens can redeem position tokens.
Vulnerability Details
Code Snippet:
AaveDIVAWrapperCore::_addLiquidity: Reference Link
function _addLiquidity(bytes32 _poolId, uint256 _collateralAmount, address _longRecipient, address _shortRecipient)
internal
{
IDIVA.Pool memory _pool = IDIVA(_diva).getPoolParameters(_poolId);
address _collateralToken = _wTokenToCollateralToken[_pool.collateralToken];
if (_collateralToken == address(0)) {
revert CollateralTokenNotRegistered();
}
_handleTokenOperations(_collateralToken, _collateralAmount, _pool.collateralToken);
@> IDIVA(_diva).addLiquidity(_poolId, _collateralAmount, _longRecipient, _shortRecipient);
}
Code Snippet:
AaveDIVAWrapperCore::_removeLiquidity: Reference Link
function _removeLiquidity(bytes32 _poolId, uint256 _positionTokenAmount, address _recipient)
internal
returns (uint256)
{
IDIVA.Pool memory _pool = IDIVA(_diva).getPoolParameters(_poolId);
if (_wTokenToCollateralToken[_pool.collateralToken] == address(0)) {
revert CollateralTokenNotRegistered();
}
IERC20Metadata _shortTokenContract = IERC20Metadata(_pool.shortToken);
IERC20Metadata _longTokenContract = IERC20Metadata(_pool.longToken);
IERC20Metadata _collateralTokenContract = IERC20Metadata(_pool.collateralToken);
uint256 _userBalanceShort = _shortTokenContract.balanceOf(msg.sender);
uint256 _userBalanceLong = _longTokenContract.balanceOf(msg.sender);
uint256 _positionTokenAmountToRemove = _positionTokenAmount;
if (_positionTokenAmount == type(uint256).max) {
_positionTokenAmountToRemove = _userBalanceShort > _userBalanceLong ? _userBalanceLong : _userBalanceShort;
}
@> _shortTokenContract.transferFrom(
@> msg.sender,
* from
*/
address(this),
* to
*/
_positionTokenAmountToRemove
);
@> _longTokenContract.transferFrom(
@> msg.sender,
* from
*/
address(this),
* to
*/
_positionTokenAmountToRemove
);
uint256 _wTokenBalanceBeforeRemoveLiquidity = _collateralTokenContract.balanceOf(address(this));
@> IDIVA(_diva).removeLiquidity(_poolId, _positionTokenAmountToRemove);
uint256 _wTokenAmountReturned =
_collateralTokenContract.balanceOf(address(this)) - _wTokenBalanceBeforeRemoveLiquidity;
uint256 _amountReturned = _redeemWTokenPrivate(
_pool.collateralToken,
_wTokenAmountReturned,
_recipient,
address(this)
);
return _amountReturned;
}
It is entirely reasonable for the msg.sender (liquidity provider) to have the ability to specify a recipient when removing liquidity, allowing them to designate who will receive the collateral upon withdrawal. This flexibility ensures that liquidity providers can manage their assets efficiently, directing funds to different addresses as needed.
However, a critical flaw arises from the fact that the liquidity provider does not retain any ownership record or control over the position tokens once they have been distributed to the designated long and short token recipients. Since removing liquidity requires burning an equal amount of long and short position tokens, the liquidity provider becomes entirely dependent on the cooperation of the token recipients to return or approve the use of those tokens.
If these recipients refuse to provide approval or become inactive, the liquidity provider is effectively blocked from reclaiming their collateral, leading to a potential liquidity lock. This oversight introduces a Denial of Service (DoS) risk, where users are unable to exit their positions simply because the recipients of their position tokens do not grant the necessary permissions.
To mitigate this issue, the protocol should implement a mechanism that either ensures liquidity providers maintain some form of reclaimable control over their distributed position tokens or introduces an alternative means for liquidity withdrawal that does not rely entirely on recipient approval. Without such safeguards, liquidity providers risk permanent loss of access to their locked funds, making the system unreliable and vulnerable to manipulation by malicious actors.
Let's analyze the problem with the help of a scenario:
The Problem: Let's assume that
Alice provides liquidity and, in return, receives both long and short position tokens.
Alice decides to transfer:
Long tokens → Bob
Short tokens → Charlie
Now, Bob and Charlie each own the respective tokens, and they've ownership and control the approval rights over their tokens.
Alice wants to remove liquidity, but to do so, she must return an equal amount of long and short tokens to the contract.
Bob and Charlie refuse to approve Alice to spend their tokens.
Alice is now stuck—she cannot retrieve her original liquidity because she does not have both long and short tokens.
Proof of Concept
To demonstrate this vulnerability, a Proof of Concept (PoC) is provided below.
Note: The PoC is written using the Foundry tool.
-
Step 1: Make a foundry project and put all the contracts in the src directory.
-
Step 2: Create a test folder and mocks folder in the test and src directories respectively (or we already have a mocks folder in src).
-
Step 3: Create all necessary mocks
-
step 4: Create a test file with any name in the test directory.
pragma solidity 0.8.26;
import {Test, console} from "forge-std/Test.sol";
import {AaveMock} from "../src/mocks/AaveMock.m.sol";
import {ATokenMock} from "../src/mocks/ATokenMock.m.sol";
import {DivaMock} from "../src/mocks/DivaMock.m.sol";
import {MockERC20} from "../src/mocks/MockERC20.sol";
import {MockLongToken} from "../src/mocks/MockLongToken.m.sol";
import {MockShortToken} from "../src/mocks/MockShortToken.m.sol";
import {MockUSDT} from "../src/mocks/MockUSDTEthereum.sol";
import {AaveDIVAWrapper} from "../src/AaveDIVAWrapper.sol";
import {WToken} from "../src/WToken.sol";
import {IAaveDIVAWrapper} from "../src/interfaces/IAaveDIVAWrapper.sol";
contract AaveDivaWrapperTest is Test {
AaveDIVAWrapper aaveDivaWrapper;
DivaMock divaMock;
AaveMock aaveMock;
MockUSDT usdtMock;
address AAVE_DIVA_WRAPPER_OWNER = makeAddr("AAVE_DIVA_WRAPPER_OWNER");
address ALICE = makeAddr("ALICE");
address BOB = makeAddr("BOB");
address CHARLIE = makeAddr("CHARLIE");
address INTRUDER = makeAddr("INTRUDER");
address CHAINLINK_DATA_PROVIDER = makeAddr("CHAINLINK_DATA_PROVIDER");
address ERC721_PERMISSIONED_TOKEN = makeAddr("ERC721_PERMISSIONED_TOKEN");
function setUp() public {
aaveMock = new AaveMock();
divaMock = new DivaMock();
usdtMock = new MockUSDT();
aaveMock.setAaveAllowedTokens(address(usdtMock));
vm.startPrank(AAVE_DIVA_WRAPPER_OWNER);
aaveDivaWrapper = new AaveDIVAWrapper(address(aaveMock), address(divaMock), AAVE_DIVA_WRAPPER_OWNER);
vm.stopPrank();
}
}
Step 5: Now, Put the following Test PoC in the test file adjacent to the setUp function.
function testLiquidityProviderLocksLiquidity() public {
uint256 gradient = (5 * (10 ** (usdtMock.decimals() - 1)));
uint256 collateralAmount = (100 * (10 ** usdtMock.decimals()));
IAaveDIVAWrapper.PoolParams memory _poolParams = IAaveDIVAWrapper.PoolParams({
referenceAsset: "USDT/USD",
expiryTime: uint96(block.timestamp + 2 hours),
floor: 100,
inflection: 150,
cap: 200,
gradient: gradient,
collateralAmount: collateralAmount,
collateralToken: address(usdtMock),
dataProvider: CHAINLINK_DATA_PROVIDER,
capacity: type(uint256).max,
longRecipient: ALICE,
shortRecipient: ALICE,
permissionedERC721Token: ERC721_PERMISSIONED_TOKEN
});
vm.startPrank(AAVE_DIVA_WRAPPER_OWNER);
address wUSDTToken = aaveDivaWrapper.registerCollateralToken(address(usdtMock));
vm.stopPrank();
usdtMock.mint(ALICE, collateralAmount);
vm.startPrank(ALICE);
usdtMock.approve(address(aaveDivaWrapper), collateralAmount);
bytes32 poolId = aaveDivaWrapper.createContingentPool(_poolParams);
vm.stopPrank();
usdtMock.mint(ALICE, collateralAmount);
vm.startPrank(ALICE);
usdtMock.approve(address(aaveDivaWrapper), 0);
usdtMock.approve(address(aaveDivaWrapper), collateralAmount);
aaveDivaWrapper.addLiquidity(poolId, collateralAmount, BOB, CHARLIE);
vm.stopPrank();
MockLongToken longToken = MockLongToken(divaMock.getPoolParameters(poolId).longToken);
MockShortToken shortToken = MockShortToken(divaMock.getPoolParameters(poolId).shortToken);
address aTokenAddress = aaveMock.getReserveData(address(usdtMock)).aTokenAddress;
ATokenMock aToken = ATokenMock(aTokenAddress);
WToken wToken = WToken(wUSDTToken);
console.log("aaveDivaWrapper aToken balance : ", aToken.balanceOf(address(aaveDivaWrapper)));
console.log("aaveDivaWrapper usdt balance : ", usdtMock.balanceOf(address(aaveDivaWrapper)));
console.log("aaveDivaWrapper wToken balance : ", wToken.balanceOf(address(aaveDivaWrapper)));
console.log("aaveDivaWrapper longToken balance : ", longToken.balanceOf(address(aaveDivaWrapper)));
console.log("aaveDivaWrapper shortToken balance: ", shortToken.balanceOf(address(aaveDivaWrapper)));
console.log("-----------------------------------");
console.log("aaveMock aToken balance : ", aToken.balanceOf(address(aaveMock)));
console.log("aaveMock usdt balance : ", usdtMock.balanceOf(address(aaveMock)));
console.log("aaveMock wToken balance : ", wToken.balanceOf(address(aaveMock)));
console.log("aaveMock longToken balance : ", longToken.balanceOf(address(aaveMock)));
console.log("aaveMock shortToken balance : ", shortToken.balanceOf(address(aaveMock)));
console.log("-----------------------------------");
console.log("diva aToken balance : ", aToken.balanceOf(address(divaMock)));
console.log("diva usdt balance : ", usdtMock.balanceOf(address(divaMock)));
console.log("diva wToken balance : ", wToken.balanceOf(address(divaMock)));
console.log("diva longToken balance : ", longToken.balanceOf(address(divaMock)));
console.log("diva shortToken balance : ", shortToken.balanceOf(address(divaMock)));
console.log("-----------------------------------");
console.log("alice aToken balance : ", aToken.balanceOf(ALICE));
console.log("alice usdt balance : ", usdtMock.balanceOf(ALICE));
console.log("alice wToken balance : ", wToken.balanceOf(ALICE));
console.log("alice longToken balance : ", longToken.balanceOf(ALICE));
console.log("alice shortToken balance : ", shortToken.balanceOf(ALICE));
console.log("-----------------------------------");
console.log("BOB aToken balance : ", aToken.balanceOf(BOB));
console.log("BOB usdt balance : ", usdtMock.balanceOf(BOB));
console.log("BOB wToken balance : ", wToken.balanceOf(BOB));
console.log("BOB longToken balance : ", longToken.balanceOf(BOB));
console.log("BOB shortToken balance : ", shortToken.balanceOf(BOB));
console.log("-----------------------------------");
console.log("CHARLIE aToken balance : ", aToken.balanceOf(CHARLIE));
console.log("CHARLIE usdt balance : ", usdtMock.balanceOf(CHARLIE));
console.log("CHARLIE wToken balance : ", wToken.balanceOf(CHARLIE));
console.log("CHARLIE longToken balance : ", longToken.balanceOf(CHARLIE));
console.log("CHARLIE shortToken balance : ", shortToken.balanceOf(CHARLIE));
vm.warp(block.timestamp + 1 days);
vm.startPrank(ALICE);
vm.expectRevert(
abi.encodeWithSelector(
bytes4(keccak256(abi.encodePacked("ERC20InsufficientAllowance(address,uint256,uint256)"))),
address(aaveDivaWrapper),
0,
longToken.balanceOf(BOB)
)
);
aaveDivaWrapper.removeLiquidity(poolId, type(uint256).max, ALICE);
vm.stopPrank();
}
Step 6: Now, to run the test, run the following commands in the terminal in sequence:
forge test --mt testLiquidityProviderLocksLiquidity -vvvv
Step 7: See the output of the test in the terminal, the output should be similar to the below one:
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/AaveDivaWrapperTest.t.sol:AaveDivaWrapperTest
[PASS] testLiquidityProviderLocksLiquidity() (gas: 3590682)
Logs:
aaveDivaWrapper aToken balance : 200000000
aaveDivaWrapper usdt balance : 0
aaveDivaWrapper wToken balance : 0
aaveDivaWrapper longToken balance : 0
aaveDivaWrapper shortToken balance: 0
-----------------------------------
aaveMock aToken balance : 0
aaveMock usdt balance : 200000000
aaveMock wToken balance : 0
aaveMock longToken balance : 0
aaveMock shortToken balance : 0
-----------------------------------
diva aToken balance : 0
diva usdt balance : 0
diva wToken balance : 200000000
diva longToken balance : 0
diva shortToken balance : 0
-----------------------------------
alice aToken balance : 0
alice usdt balance : 0
alice wToken balance : 0
alice longToken balance : 100000000
alice shortToken balance : 100000000
-----------------------------------
BOB aToken balance : 0
BOB usdt balance : 0
BOB wToken balance : 0
BOB longToken balance : 100000000
BOB shortToken balance : 0
-----------------------------------
CHARLIE aToken balance : 0
CHARLIE usdt balance : 0
CHARLIE wToken balance : 0
CHARLIE longToken balance : 0
CHARLIE shortToken balance : 100000000
Traces:
[3817982] AaveDivaWrapperTest::testLiquidityProviderLocksLiquidity()
├─ [266] MockUSDT::decimals() [staticcall]
│ └─ ← [Return] 6
├─ [266] MockUSDT::decimals() [staticcall]
│ └─ ← [Return] 6
├─ [0] VM::startPrank(AAVE_DIVA_WRAPPER_OWNER: [0xE3956835e40e4731A03596C87534501ac1656f1E])
│ └─ ← [Return]
.
.
.
├─ [540] ATokenMock::balanceOf(AaveDIVAWrapper: [0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986]) [staticcall]
│ └─ ← [Return] 200000000 [2e8]
├─ [0] console::log("aaveDivaWrapper aToken balance : ", 200000000 [2e8]) [staticcall]
│ └─ ← [Stop]
├─ [562] MockUSDT::balanceOf(AaveDIVAWrapper: [0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveDivaWrapper usdt balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [540] WToken::balanceOf(AaveDIVAWrapper: [0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveDivaWrapper wToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] MockLongToken::balanceOf(AaveDIVAWrapper: [0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveDivaWrapper longToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] MockShortToken::balanceOf(AaveDIVAWrapper: [0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveDivaWrapper shortToken balance: ", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("-----------------------------------") [staticcall]
│ └─ ← [Stop]
├─ [2540] ATokenMock::balanceOf(AaveMock: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveMock aToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [562] MockUSDT::balanceOf(AaveMock: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ └─ ← [Return] 200000000 [2e8]
├─ [0] console::log("aaveMock usdt balance : ", 200000000 [2e8]) [staticcall]
│ └─ ← [Stop]
├─ [2540] WToken::balanceOf(AaveMock: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveMock wToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] MockLongToken::balanceOf(AaveMock: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveMock longToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] MockShortToken::balanceOf(AaveMock: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("aaveMock shortToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("-----------------------------------") [staticcall]
│ └─ ← [Stop]
├─ [2540] ATokenMock::balanceOf(DivaMock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("diva aToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2562] MockUSDT::balanceOf(DivaMock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("diva usdt balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [540] WToken::balanceOf(DivaMock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ └─ ← [Return] 200000000 [2e8]
├─ [0] console::log("diva wToken balance : ", 200000000 [2e8]) [staticcall]
│ └─ ← [Stop]
├─ [540] MockLongToken::balanceOf(DivaMock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("diva longToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [540] MockShortToken::balanceOf(DivaMock: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("diva shortToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("-----------------------------------") [staticcall]
│ └─ ← [Stop]
├─ [2540] ATokenMock::balanceOf(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("alice aToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [562] MockUSDT::balanceOf(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("alice usdt balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] WToken::balanceOf(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("alice wToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [540] MockLongToken::balanceOf(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ └─ ← [Return] 100000000 [1e8]
├─ [0] console::log("alice longToken balance : ", 100000000 [1e8]) [staticcall]
│ └─ ← [Stop]
├─ [540] MockShortToken::balanceOf(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ └─ ← [Return] 100000000 [1e8]
├─ [0] console::log("alice shortToken balance : ", 100000000 [1e8]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("-----------------------------------") [staticcall]
│ └─ ← [Stop]
├─ [2540] ATokenMock::balanceOf(BOB: [0xa53b369bDCbe05dcBB96d6550C924741902d2615]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("BOB aToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2562] MockUSDT::balanceOf(BOB: [0xa53b369bDCbe05dcBB96d6550C924741902d2615]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("BOB usdt balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] WToken::balanceOf(BOB: [0xa53b369bDCbe05dcBB96d6550C924741902d2615]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("BOB wToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [540] MockLongToken::balanceOf(BOB: [0xa53b369bDCbe05dcBB96d6550C924741902d2615]) [staticcall]
│ └─ ← [Return] 100000000 [1e8]
├─ [0] console::log("BOB longToken balance : ", 100000000 [1e8]) [staticcall]
│ └─ ← [Stop]
├─ [2540] MockShortToken::balanceOf(BOB: [0xa53b369bDCbe05dcBB96d6550C924741902d2615]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("BOB shortToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("-----------------------------------") [staticcall]
│ └─ ← [Stop]
├─ [2540] ATokenMock::balanceOf(CHARLIE: [0x05D932dE3D050b91d0E14ac70fc4a0CC06D45af9]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("CHARLIE aToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2562] MockUSDT::balanceOf(CHARLIE: [0x05D932dE3D050b91d0E14ac70fc4a0CC06D45af9]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("CHARLIE usdt balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] WToken::balanceOf(CHARLIE: [0x05D932dE3D050b91d0E14ac70fc4a0CC06D45af9]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("CHARLIE wToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [2540] MockLongToken::balanceOf(CHARLIE: [0x05D932dE3D050b91d0E14ac70fc4a0CC06D45af9]) [staticcall]
│ └─ ← [Return] 0
├─ [0] console::log("CHARLIE longToken balance : ", 0) [staticcall]
│ └─ ← [Stop]
├─ [540] MockShortToken::balanceOf(CHARLIE: [0x05D932dE3D050b91d0E14ac70fc4a0CC06D45af9]) [staticcall]
│ └─ ← [Return] 100000000 [1e8]
├─ [0] console::log("CHARLIE shortToken balance : ", 100000000 [1e8]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::warp(86401 [8.64e4])
│ └─ ← [Return]
├─ [0] VM::startPrank(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916])
│ └─ ← [Return]
├─ [540] MockLongToken::balanceOf(BOB: [0xa53b369bDCbe05dcBB96d6550C924741902d2615]) [staticcall]
│ └─ ← [Return] 100000000 [1e8]
├─ [0] VM::expectRevert(ERC20InsufficientAllowance(0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986, 0, 100000000 [1e8]))
│ └─ ← [Return]
├─ [16244] AaveDIVAWrapper::removeLiquidity(0x6e5b574bbb039056fe6dab15818fd206ed5e73dc75e9b454bd29d5ee9fd6142b, 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916])
│ ├─ [4624] DivaMock::getPoolParameters(0x6e5b574bbb039056fe6dab15818fd206ed5e73dc75e9b454bd29d5ee9fd6142b) [staticcall]
│ │ └─ ← [Return] Pool({ floor: 100, inflection: 150, cap: 200, gradient: 500000 [5e5], collateralBalance: 200000000 [2e8], finalReferenceValue: 0, capacity: 115792089237316195423570985008687907853269984665640564039457584007913129639935 [1.157e77], statusTimestamp: 1, shortToken: 0x8d2C17FAd02B7bb64139109c6533b7C2b9CADb81, payoutShort: 0, longToken: 0xffD4505B3452Dc22f8473616d50503bA9E1710Ac, payoutLong: 0, collateralToken: 0x966D842bc56C5c4443591D3bb0B565d06c473D4B, expiryTime: 7201, dataProvider: 0x1e6974FFaa047925EfcADEc7382f1652E3e06d95, indexFees: 0, indexSettlementPeriods: 0, statusFinalReferenceValue: 0, referenceAsset: "USDT/USD" })
│ ├─ [540] MockShortToken::balanceOf(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ │ └─ ← [Return] 100000000 [1e8]
│ ├─ [540] MockLongToken::balanceOf(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916]) [staticcall]
│ │ └─ ← [Return] 100000000 [1e8]
│ ├─ [2974] MockShortToken::transferFrom(ALICE: [0xef211076B8d8b46797E09c9a374Fb4Cdc1dF0916], AaveDIVAWrapper: [0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986], 100000000 [1e8])
│ │ └─ ← [Revert] ERC20InsufficientAllowance(0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986, 0, 100000000 [1e8])
│ └─ ← [Revert] ERC20InsufficientAllowance(0x93Cf436DE8BF36C3d42Ab9dDD57631eCe44F9986, 0, 100000000 [1e8])
├─ [0] VM::stopPrank()
│ └─ ← [Return]
└─ ← [Stop]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.48ms (1.47ms CPU time)
Ran 1 test suite in 1.67s (3.48ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
As demonstrated in the output, test passes and removeLiquidity function reverts with error ERC20InsufficientAllowance.
Impact
Tools Used
Recommendations
To mitigate this issue, a few possible solutions exist:
-
Implement an "Atomic Transfer Mechanism"
Example: The addLiquidity function could include an internal accounting system where Alice retains a right to reclaim tokens instead of immediately transferring ownership.
-
Approval to recipients
The Liquidity Provider may authorize Position Token recipients to spend Position Tokens on their behalf. Additionally, there should be a confirmation mechanism that verifies if the Liquidity Provider has sufficient Position Tokens. If not, the logic should check whether the recipients have already redeemed Position Tokens. If they have, the system should revert the transaction with an appropriate message.
-
Create an "Emergency Exit" Function
-
If Liquidity Provider is unable to retrieve their tokens, the protocol could allow an alternative withdrawal mechanism:
-
After a timelock or governance approval, Liquidity Provider could forcibly reclaim an equivalent value of collateral and a forcibly forfeiture from recipients approval.
-
This would prevent funds from being permanently locked.
-
Set msg.sender as both the longTokenRecipient and shortTokenRecipient.
-
Since both long and short position tokens are ERC20-based tokens, it is logical to assign msg.sender as the initial recipient. When an ERC20 token is owned by a user, they have full control over transferring or distributing it as they see fit. This flexibility eliminates the need to pre-define separate recipients at the time of adding liquidity.
-
By ensuring that msg.sender initially receives both position tokens, the liquidity provider retains complete ownership and control, allowing them to later transfer or allocate the tokens to different recipients at their discretion.
Why is this important?
If different recipients are pre-selected for long and short tokens, it immediately removes control from the liquidity provider, potentially leading to liquidity lock scenarios where they are unable to remove liquidity if the recipients refuse to return the tokens or approve transfers.
By assigning both position tokens to msg.sender by default, the liquidity provider retains ownership and can manage their assets efficiently without any unintended restrictions.