Missing Reentrancy Guard in Critical Functions
Issue:
The claimAndSwap and _freeFunds functions interact with external contracts (transmuter, router) without implementing reentrancy protection. If these external calls allow reentrancy, they could be exploited to manipulate the contract's state and logic, leading to potential fund drains or operational disruption.
Impact:
-
Fund Drains:
-
State Corruption:
-
Operational Halt:
Evidence from Code:
claimAndSwap Function:
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) external onlyKeepers {
transmuter.claim(_amountClaim, address(this));
uint256 balBefore = asset.balanceOf(address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
_freeFunds Function:
function _freeFunds(uint256 _amount) internal override {
uint256 totalAvailable = transmuter.getUnexchangedBalance(address(this));
if (_amount > totalAvailable) {
transmuter.withdraw(totalAvailable, address(this));
} else {
transmuter.withdraw(_amount, address(this));
}
}
Attack Path PoC
-
Reentrancy via transmuter.claim:
Assume an attacker deploys a malicious transmuter contract that allows reentrant calls during the claim function execution.
-
Attack Setup:
The attacker calls claimAndSwap with their address as the recipient and malicious transmuter logic to reenter the claimAndSwap function before state variables like balBefore or balances are updated.
-
Implementation:
pragma solidity ^0.8.18;
contract MaliciousTransmuter {
address public strategy;
constructor(address _strategy) {
strategy = _strategy;
}
function claim(uint256 _amount, address _to) external {
StrategyOp(strategy).claimAndSwap(_amount, _amount, new IVeloRouter.route );
}
function getClaimableBalance(address _owner) external pure returns (uint256) {
return type(uint256).max;
}
}
-
Execution Flow:
-
The attacker sets the malicious transmuter contract in the strategy.
-
Calls claimAndSwap:
strategy.claimAndSwap(1000, 900, path);
-
The malicious transmuter reenters claimAndSwap before the state updates, exploiting incomplete logic to drain funds or cause unexpected behavior.
-
Result:
Funds can be drained or state variables corrupted due to unprotected reentrant calls.
Mitigation
Implement Reentrancy Guards:
Use OpenZeppelin's ReentrancyGuard to protect critical functions.
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract StrategyOp is BaseStrategy, ReentrancyGuard {
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) external nonReentrant onlyKeepers {
uint256 claimable = transmuter.getClaimableBalance(address(this));
require(_amountClaim <= claimable, "Claim amount exceeds balance");
uint256 balBefore = asset.balanceOf(address(this));
transmuter.claim(_amountClaim, address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require((balAfter - balBefore) >= _minOut, "Slippage too high");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}
function _freeFunds(uint256 _amount) internal override nonReentrant {
uint256 totalAvailable = transmuter.getUnexchangedBalance(address(this));
if (_amount > totalAvailable) {
transmuter.withdraw(totalAvailable, address(this));
} else {
transmuter.withdraw(_amount, address(this));
}
}
}
Use Checks-Effects-Interactions Pattern:
Update internal state variables before making external calls.
function claimAndSwap(
uint256 _amountClaim,
uint256 _minOut,
IVeloRouter.route[] calldata _path
) external nonReentrant onlyKeepers {
uint256 claimable = transmuter.getClaimableBalance(address(this));
require(_amountClaim <= claimable, "Claim amount exceeds balance");
uint256 balBefore = asset.balanceOf(address(this));
uint256 newBalance = balBefore + _amountClaim;
transmuter.claim(_amountClaim, address(this));
_swapUnderlyingToAsset(_amountClaim, _minOut, _path);
uint256 balAfter = asset.balanceOf(address(this));
require(balAfter >= newBalance, "Balance mismatch after swap");
transmuter.deposit(asset.balanceOf(address(this)), address(this));
}