Flashloan exploit¶
Adding flashloan capability¶
The new token code is at MinimalLendingToken.sol.
It defines an interface the borrowing contract needs to implement:
interface IFlashLoanReceiver { function execute() external payable; }
It adds a
flashLoan
function:function flashLoan(uint256 amount) public { uint256 preBalance = address(this).balance; require(preBalance >= amount, "Insufficient funds"); IFlashLoanReceiver(msg.sender).execute{value: amount}(); uint256 postBalance = address(this).balance; require(postBalance >= preBalance, "Loan has not been repaid"); }
Creating a borrower¶
We use the Prover’s over-approximation of the storage to create a contract that “might do anything”.
Simplified example¶
uint8 public storageVar;
uint256 public amount;
function doSomething() public {
if (storageVar == 0) { // Deposit
token.deposit{value: amount}(amount);
} else if (storageVar == 1) { // Loan
token.flashLoan(amount);
} else if (storageVar == 2) { // Repay
payable(address(token)).send(amount);
} else { // Vote
voting.vote(booleans[i]);
}
}
By default, the Prover makes no assumptions on the values of
storageVar
andamount
.Therefore
doSomething
might do any of the actions specified.
Borrower code¶
This is a more sophisticated example, which allows it to do two actions at once.
FlashLoanReceiverHarness.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {IFlashLoanReceiver, MinimalToken} from "./MinimalLendingToken.sol";
import {PowerVoting} from "./PowerVoting.sol";
// A contract that may do anything.
// We use the Provver's over-approximation of storage for enabling any possible action.
contract FlashLoanReceiverHarness is IFlashLoanReceiver {
MinimalToken public token;
PowerVoting public voting;
// Which function to call
enum Func {
Deposit,
FlashLoan,
Vote,
DoNothing
}
mapping(uint8 => Func) public toCalls;
// The amounts to use in the function calls
mapping(uint8 => uint256) public amounts;
// Boolean values to use
mapping(uint8 => bool) public booleans;
// The iteration number
uint8 public i;
function doSingleSomething() public {
i += 1; // Increase iteration
if (toCalls[i] == Func.Deposit) {
token.deposit{value: amounts[i]}(amounts[i]);
} else if (toCalls[i] == Func.FlashLoan) {
token.flashLoan(amounts[i]);
} else if (toCalls[i] == Func.DoNothing) {
// Do nothing
} else {
voting.vote(booleans[i]);
}
}
function doSomethings() public {
for (uint8 j; j < 2; j++) {
doSingleSomething();
}
}
function execute() external payable override {
doSomethings();
}
}
Revisiting tokenSolvency
¶
/// @title Total supply not greater than ETH balance
invariant tokenSolvency()
nativeBalances[token] >= token.totalSupply();
Exercises¶
Task 10: Understand the violation¶
Run the config LenderPowerVotingInvariant.conf to check whether
tokenSolvency
still holds.Why can we not verify the property anymore? Can you see the scenario in the call trace that is problematic?
Here is the run report: invariant violation report.
Task 11: Checking if exploits are possible¶
satisfy balanceOf(user1) > balanceOf(user2);
We cannot only use assert
statements to verify properties, but we can also check whether
there is a path along the execution of contract functions that allows for certain exploits to be applied to our contracts.
To this end, we can use satisfy
statements that check if there is at least one viable path that allows
the execution of some predefined scenario.
Use the satisfy
keyword to make the Prover search for a positive example that represents a violation of the following invariant:
Property: total votes are less than or equal to total eth in the system.
Hint
Can you think of a flashloan exploit to increase your voting power?
Solution
/// @title An exploit example
rule example() {
// The following requirements are just to make it easier to see the problem
require totalVotes() == 0;
require nativeBalances[borrower] == 0;
require token.balanceOf(borrower) == 0;
// The total ETH in the system before
mathint totalBalances = nativeBalances[token] + nativeBalances[borrower];
env e;
borrower.doSomethings(e);
// More votes than total ETH
satisfy to_mathint(totalVotes()) > totalBalances;
}
Find the prover report link: rule report.