<100 subscribers

There's a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.
Drain all ETH funds from the user's contract. Doing it in a single transaction is a big plus ;)
NaiveReceiverLenderPool.sol

uint256 private constant FIXED_FEE
It is assigned a value of 1 ether and has a constant variable characteristic, so it cannot be modified as a fixed value at compile-time, and it is private and cannot be accessed from outside.
State variables can be defined as constant and immutable. Both types of variables cannot be modified after they are constructed. Constant variables are fixed at compile-time and cannot be altered, while immutable variables can be assigned a value at compile-time but cannot be changed afterwards.
function fixedFee() external pure returns (uint256)
You can see that the State Variable value is returned as it is without modifying the state value. Used for checking the FIXED_FEE value.
function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant
Execute the flash loan logic, which is a core function in the current contract.
The local variable balanceBefore is assigned the current contract balance value.
✅ The balanceBefore value must be greater than or equal to the borrowAmount parameter, and the balance of the current contract must be higher than the borrowing price in order to borrow.
✅ The borrower parameter is assigned an address type. Under the current condition, the isContract function of the openzeppelin internal address contract is called to determine the size of the target address and check whether the contract is active.
The isContract function has the following characteristics:
It is not safe to assume that an address for which this function returns false is an External Owner Account instead of a contract.
It relies on extcodesize returning 0 for the contract being created because the code is only saved when the constructor execution ends.
extcodesize returns 0 if called in the contract constructor.
If you are using this in a security-sensitive setup, you should consider if this is a problem.
The functionCallWithValue method is called with the borrower parameter. In the low-level stage, the receiveEther(uint256) method of the target contract FlashLoanReceiver is called and used as the FIXED_FEE parameter and borrowAmount value.
✅ After the flash loan logic is completed, it is checked whether the current contract’s balance value is greater than or equal to the balanceBefore+FIXED_FEE value to determine whether the progress has been made normally.
FlashLoanReceiver.sol

address payable private pool
This is the contract address used for pool management in the flash loan process.
constructor(address payable poolAddress)
When creating a distribution, it's possible to assign a contract address to the state variable "pool" according to the value of the parameter.
function receiveEther(uint256 fee) pulibc payable
✅ The address value of the msg.sender address and the pool variable must be the same, and you can confirm that the sender is the contract address assigned to the pool variable.
After adding the fee parameter value and msg.value value, it is assigned to the amountToBeRepaid variable.
The value assigned to the fee is 1 ether, which is the fixed fee value assigned in the NaiveReceiverLenderPool contract. Even if the msg.value value changes, you can see that the fee value is fixed.
✅ The current contract's balance value must be greater than or equal to the amountToBeRepaid value to pass the condition, and the borrow amount value must not be greater than the balance in the holding pool.
Call the meaningless _executeActionDuringFlashLoan() function (gas waste).
Use the openzeppelin library Address contract method sendValue to send the loan return to the NaiveReceiverLenderPool contract.
Vulnerable targets can create problems in the logic of flash loans. Because the flashLoan function can be called externally, there is no way to reject a large number of incoming loans by calling the Receiver with this address. By providing the address you choose to pass on to the borrower to accept the flashLoan, you can charge a fee and generate revenue from the flash loan, which can potentially ruin that Receiver's pool.

The receiver contract was previously assigned 10 ethers, and all conditions can be satisfied if a contract call is made based on the first factor of the function NaiveReceiverLenderPool.sol:flashLoan. Based on this, when the value of msg.value is set to 0 and the contract is called, the receiver contract can return the allocated balance plus the calculated msg.value + fee by making a contract call. This can potentially be used to carry out attacks.

Thank you for the @tinchoabbate that made a good wargame.

There's a lending pool offering quite expensive flash loans of Ether, which has 1000 ETH in balance.
You also see that a user has deployed a contract with 10 ETH in balance, capable of interacting with the lending pool and receiveing flash loans of ETH.
Drain all ETH funds from the user's contract. Doing it in a single transaction is a big plus ;)
NaiveReceiverLenderPool.sol

uint256 private constant FIXED_FEE
It is assigned a value of 1 ether and has a constant variable characteristic, so it cannot be modified as a fixed value at compile-time, and it is private and cannot be accessed from outside.
State variables can be defined as constant and immutable. Both types of variables cannot be modified after they are constructed. Constant variables are fixed at compile-time and cannot be altered, while immutable variables can be assigned a value at compile-time but cannot be changed afterwards.
function fixedFee() external pure returns (uint256)
You can see that the State Variable value is returned as it is without modifying the state value. Used for checking the FIXED_FEE value.
function flashLoan(address borrower, uint256 borrowAmount) external nonReentrant
Execute the flash loan logic, which is a core function in the current contract.
The local variable balanceBefore is assigned the current contract balance value.
✅ The balanceBefore value must be greater than or equal to the borrowAmount parameter, and the balance of the current contract must be higher than the borrowing price in order to borrow.
✅ The borrower parameter is assigned an address type. Under the current condition, the isContract function of the openzeppelin internal address contract is called to determine the size of the target address and check whether the contract is active.
The isContract function has the following characteristics:
It is not safe to assume that an address for which this function returns false is an External Owner Account instead of a contract.
It relies on extcodesize returning 0 for the contract being created because the code is only saved when the constructor execution ends.
extcodesize returns 0 if called in the contract constructor.
If you are using this in a security-sensitive setup, you should consider if this is a problem.
The functionCallWithValue method is called with the borrower parameter. In the low-level stage, the receiveEther(uint256) method of the target contract FlashLoanReceiver is called and used as the FIXED_FEE parameter and borrowAmount value.
✅ After the flash loan logic is completed, it is checked whether the current contract’s balance value is greater than or equal to the balanceBefore+FIXED_FEE value to determine whether the progress has been made normally.
FlashLoanReceiver.sol

address payable private pool
This is the contract address used for pool management in the flash loan process.
constructor(address payable poolAddress)
When creating a distribution, it's possible to assign a contract address to the state variable "pool" according to the value of the parameter.
function receiveEther(uint256 fee) pulibc payable
✅ The address value of the msg.sender address and the pool variable must be the same, and you can confirm that the sender is the contract address assigned to the pool variable.
After adding the fee parameter value and msg.value value, it is assigned to the amountToBeRepaid variable.
The value assigned to the fee is 1 ether, which is the fixed fee value assigned in the NaiveReceiverLenderPool contract. Even if the msg.value value changes, you can see that the fee value is fixed.
✅ The current contract's balance value must be greater than or equal to the amountToBeRepaid value to pass the condition, and the borrow amount value must not be greater than the balance in the holding pool.
Call the meaningless _executeActionDuringFlashLoan() function (gas waste).
Use the openzeppelin library Address contract method sendValue to send the loan return to the NaiveReceiverLenderPool contract.
Vulnerable targets can create problems in the logic of flash loans. Because the flashLoan function can be called externally, there is no way to reject a large number of incoming loans by calling the Receiver with this address. By providing the address you choose to pass on to the borrower to accept the flashLoan, you can charge a fee and generate revenue from the flash loan, which can potentially ruin that Receiver's pool.

The receiver contract was previously assigned 10 ethers, and all conditions can be satisfied if a contract call is made based on the first factor of the function NaiveReceiverLenderPool.sol:flashLoan. Based on this, when the value of msg.value is set to 0 and the contract is called, the receiver contract can return the allocated balance plus the calculated msg.value + fee by making a contract call. This can potentially be used to carry out attacks.

Thank you for the @tinchoabbate that made a good wargame.
Share Dialog
Share Dialog
No comments yet