RACE #16 Of The Secureum Bootcamp Epoch∞

This is a Write-Up of RACE-16, Quiz of the Secureum Bootcamp (opens in a new tab) for Ethereum Smart Contract Auditors. It was designed by recently joined Secureum Mentor Jon Stephens (opens in a new tab) and his company Veridise (opens in a new tab).

Participants of this quiz had to answer 8 questions within the strict time limit of 16 minutes. If you’re reading this in preparation for participating yourself, it’s best to give it a try under the same time limit!

As usual, I waited for submissions to close before publishing it and, to stay true to the original, I omitted syntax highlighting. Feel free to copy it into your favorite editor, but do so after starting the timer!

March 25, 2023 by patrickd


Code

All 8 questions in this RACE are based on the below contract. This is the same contract you will see for all the 8 questions in this RACE. The question is below the shown contract.

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;


import "@openzeppelin/contracts/interfaces/IERC3156FlashLender.sol";
import "@openzeppelin/contracts/interfaces/IERC20.sol";


contract FlashLoan is IERC3156FlashLender {
   bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
   uint256 public fee;


   /**
    * @param fee_ the fee that should be paid on a flashloan
    */
   constructor (
       uint256 fee_
   ) {
       fee = fee_;
   }


   /**
    * @dev The amount of currency available to be lent.
    * @param token The loan currency.
    * @return The amount of `token` that can be borrowed.
    */
   function maxFlashLoan(
       address token
   ) public view override returns (uint256) {
       return IERC20(token).balanceOf(address(this));
   }


   /**
    * @dev The fee to be charged for a given loan.
    * @param token The loan currency. Must match the address of this contract.
    * @param amount The amount of tokens lent.
    * @return The amount of `token` to be charged for the loan, on top of the returned principal.
    */
   function flashFee(
       address token,
       uint256 amount
   ) external view override returns (uint256) {
       return fee;
   }


   /**
    * @dev Loan `amount` tokens to `receiver`, and takes it back plus a `flashFee` after the ERC3156 callback.
    * @param receiver The contract receiving the tokens, needs to implement the `onFlashLoan(address user, uint256 amount, uint256 fee, bytes calldata)` interface.
    * @param token The loan currency. Must match the address of this contract.
    * @param amount The amount of tokens lent.
    * @param data A data parameter to be passed on to the `receiver` for any custom use.
    */
   function flashLoan(
       IERC3156FlashBorrower receiver,
       address token,
       uint256 amount,
       bytes calldata data
   ) external override returns (bool){
       uint256 oldAllowance = IERC20(token).allowance(address(this), address(receiver));
       uint256 oldBal = IERC20(token).balanceOf(address(this));
       require(amount <= oldBal, "Too many funds requested");
       IERC20(token).approve(address(receiver), oldAllowance + amount);

       require(
           receiver.onFlashLoan(msg.sender, token, amount, fee, data) == CALLBACK_SUCCESS,
           "Callback failed"
       );

       uint256 newBal = IERC20(token).balanceOf(address(this));
       if(newBal < oldBal + fee) {
           uint retAmt = oldBal + fee - newBal;
           require(IERC20(token).transferFrom(msg.sender, address(this), retAmt), "All funds not returned");
       }

       if (IERC20(token).allowance(address(this), address(receiver)) > oldAllowance) {
           IERC20(token).approve(address(receiver), oldAllowance);
       }

       return true;
   }
}

Question 1 of 8

Which of the following is an explanation of why flashLoan() could revert?

  • A. The transaction reverts because a user requested to borrow more than maxFlashLoan()
  • B. The transaction reverts because the receiver’s onFlashLoan() did not return CALLBACK_SUCCESS
  • C. The transaction reverts because the user returned more than retAmt funds
  • D. The transaction reverts because a user tried to spend more funds than their allowance in onFlashLoan()
Solution

Correct is A, B, D.

  • A. Is implicitly checked in the require with message "Too many funds requested"
  • B. Is explicitly checked in the require with message "Callback failed"
  • C. The user may return more but not less than retAmt
  • D. An ERC20 transferFrom() would revert in that case

Question 2 of 8

If the FlashLoan contract were safe, which of the following invariants should hold at the end of any given transaction for some ERC20 token t?

Note: old(expr) evaluates expr at the beginning of the transaction.

  • A. t.balanceOf(address(this)) >= old(t.balanceOf(address(this)))
  • B. t.balanceOf(address(this)) == old(t.balanceOf(address(this)))
  • C. t.balanceOf(address(this)) > old(t.balanceOf(address(this)))
  • D. t.balanceOf(address(this)) == old(t.balanceOf(address(this))) + fee
Solution

Correct is A.

For the flashloan to be safe, the contract's token balance must be maintained no matter which function is called. It must be (A) because flashloan will cause the token balance to either increase or stay the same (depending on fee) and all other functions should maintain token balances


Question 3 of 8

Which of the following tokens would be unsafe for the above contract to loan as doing so could result in theft?

  • A. ERC223
  • B. ERC677
  • C. ERC777
  • D. ERC1155
Solution

Correct is C.

This can be attacked by ERC20 contracts with sender-callbacks in transferFrom(). ERC777 and ERC1155 are the only ones with callbacks. But ERC1155 will revert as its APIs doesn't match the ones in the flashloan contract, even even then, it would only have receiver-callbacks.


Question 4 of 8

Which external call made by flashLoan() could result in theft if the token(s) identified in the previous question were to be used?

  • A. onFlashLoan()
  • B. balanceOf()
  • C. transferFrom()
  • D. approve()
Solution

Correct is C.

ERC777 tokens have potentially dangerous callbacks on transfer()/transferFrom() that can result in theft.


Question 5 of 8

What is the purpose of the fee in the FlashLoan contract as is?

  • A. To increase the size of available flashloans over time
  • B. To pay the owner of the flashloan contract
  • C. To pay those who staked their funds to be flashloaned
  • D. It has no purpose
Solution

Correct is A.

In the current FlashLoan contract, as it is, the sole purpose of the fee is to increase the available funds to loan.


Question 6 of 8

Which of the following describes the behavior of maxFlashLoan for a standard ERC20 token over time?

  • A. strictly-increasing
  • B. non-decreasing
  • C. constant
  • D. None of the above
Solution

Correct is B.

Can't be A since fee may be 0. It could still increase from people injecting tokens, but it may never decrease.


Question 7 of 8

For some arbitrary ERC20 token t, which of the following accurately describes the FlashLoan contract’s balance of t after a successful (i.e. non-reverting) call to flashLoan() (where t is the token requested for the flashloan):

  • A. The FlashLoan contract's balance of token t will INCREASE OR STAY THE SAME.
  • B. The FlashLoan contract's balance of token t will DECREASE OR STAY THE SAME.
  • C. The FlashLoan contract's balance of token t will STAY THE SAME.
  • D. None of the above.
Solution

Correct is D.

flashLoan() can hypothetically finish successfully with any token that implements the ERC20 interface, even if it is a bogus implementation. Therefore, there are no guarantees on the output of IERC20(token).balanceOf(user).


Question 8 of 8

Which of the following are guaranteed to hold after a successful (i.e., non-reverting) execution of flashLoan(), assuming the token for which the flashloan is requested uses OpenZeppelin’s Standard ERC20 implementation?

  • A. The receiver’s balance of “token” increases
  • B. The funds that the FlashLoan contract has approved the receiver to spend has either stayed the same or decreased.
  • C. The sum of all flashloans granted by the FlashLoan contract is less than the maxFlashLoan amount.
  • D. The token balance of any contract/user other than the FlashLoan contract, the caller of the flashLoan(), and the “receiver” contract will remain the same as before the call to flashLoan().
Solution

Correct is B.

  • A. The token balance of the receiver could increase, decrease, or remain the same in the call to onFlashLoan()
  • B. This is ensured by the last if-statement
  • C. maxFlashLoan has no relationship to the sum of all flashloans
  • D. Other user token balances could be adjusted in the call to onFlashLoan()