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));
}