Skip to content

Commit

Permalink
Merge pull request #23 from dragonfly-xyz/reentrancy-flash-loans
Browse files Browse the repository at this point in the history
Reentrancy + Flash Loans
  • Loading branch information
merklejerk authored Nov 15, 2023
2 parents 9fafbe6 + 3aa9a93 commit 1d725a9
Show file tree
Hide file tree
Showing 60 changed files with 1,004 additions and 55 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ This repo is an ongoing collection of useful, and occasionally clever, solidity/
- The consequences of interacting with contracts vs regular wallets, and how to identify them.
- [Factory Proofs](./patterns/factory-proofs)
- Proving on-chain that a contract was deployed by a trusted deployer.
- [Flash Loans](./patterns/flash-loans/)
- Designing a basic flash loan mechanism.
- [Initializing Upgradeable Contracts](./patterns/initializing-upgradeable-contracts)
- Methods to safely and efficiently initialize state for proxy contracts.
- [Merkle Proofs](./patterns/merkle-proofs)
Expand All @@ -49,6 +51,8 @@ This repo is an ongoing collection of useful, and occasionally clever, solidity/
- Transfer tokens securely without a direct allowance, in a way that works for all (legacy and modern) ERC20s.
- [Read-Only Delegatecall](./patterns/readonly-delegatecall)
- Execute arbitrary delegatecalls in your contract in a read-only manner, without side-effects.
- [Reentrancy](./patterns/reentrancy)
- Explaining reentrancy vulnerabilities and patterns for addressing them.
- [Separate Allowance Targets](./patterns/separate-allowance-targets/)
- Avoid having to migrate user allowances between upgrades with a dedicated approval contract.
- [Stack-Too-Deep Workarounds](./patterns/stack-too-deep/)
Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
src = 'patterns'
out = 'out'
libs = ['lib']
ignored_error_codes=[3628,5159]

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 50 files
+134 −0 .github/workflows/ci.yml
+29 −0 .github/workflows/sync.yml
+0 −26 .github/workflows/tests.yml
+1 −1 .gitignore
+1 −1 LICENSE-APACHE
+1 −1 LICENSE-MIT
+8 −4 README.md
+21 −0 foundry.toml
+1 −1 lib/ds-test
+16 −0 package.json
+35 −0 src/Base.sol
+21 −33 src/Script.sol
+376 −0 src/StdAssertions.sol
+244 −0 src/StdChains.sol
+817 −0 src/StdCheats.sol
+15 −0 src/StdError.sol
+107 −0 src/StdInvariant.sol
+183 −0 src/StdJson.sol
+43 −0 src/StdMath.sol
+378 −0 src/StdStorage.sol
+333 −0 src/StdStyle.sol
+198 −0 src/StdUtils.sol
+31 −777 src/Test.sol
+710 −152 src/Vm.sol
+406 −386 src/console2.sol
+105 −0 src/interfaces/IERC1155.sol
+12 −0 src/interfaces/IERC165.sol
+43 −0 src/interfaces/IERC20.sol
+190 −0 src/interfaces/IERC4626.sol
+164 −0 src/interfaces/IERC721.sol
+73 −0 src/interfaces/IMulticall3.sol
+13,248 −0 src/safeconsole.sol
+0 −12 src/test/Script.t.sol
+0 −599 src/test/StdAssertions.t.sol
+0 −226 src/test/StdCheats.t.sol
+0 −200 src/test/StdMath.t.sol
+1,015 −0 test/StdAssertions.t.sol
+216 −0 test/StdChains.t.sol
+610 −0 test/StdCheats.t.sol
+15 −21 test/StdError.t.sol
+212 −0 test/StdMath.t.sol
+120 −126 test/StdStorage.t.sol
+110 −0 test/StdStyle.t.sol
+342 −0 test/StdUtils.t.sol
+15 −0 test/Vm.t.sol
+10 −0 test/compilation/CompilationScript.sol
+10 −0 test/compilation/CompilationScriptBase.sol
+10 −0 test/compilation/CompilationTest.sol
+10 −0 test/compilation/CompilationTestBase.sol
+187 −0 test/fixtures/broadcast.log.json
2 changes: 1 addition & 1 deletion lib/solmate
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// Abstract base for common functionality between
// `ApproveRestrictedWallet` and `ApproveRestrictedWallet_Memory`.
Expand Down
2 changes: 1 addition & 1 deletion patterns/basic-proxies/ProxyWallet.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// The proxy contract for a wallet.
contract WalletProxy {
Expand Down
2 changes: 1 addition & 1 deletion patterns/big-data-storage/OnChainPfp.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// An ERC721-like NFT contract where users can mint a token with
// custom image metadata, stored on-chain.
Expand Down
2 changes: 1 addition & 1 deletion patterns/commit-reveal/SealedAuctionMint.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// An "NFT" contract that holds a sealed bid auction every 24 hours for the right
// to mint a token. The auction is a sealed auction using commit reveal to hide
Expand Down
2 changes: 1 addition & 1 deletion patterns/eip712-signed-messages/ERC721.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// Basic ERC721 contract that supports minting a specific token ID.
contract ERC721 {
Expand Down
2 changes: 1 addition & 1 deletion patterns/eip712-signed-messages/MintVouchers.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

import "./ERC721.sol";

Expand Down
2 changes: 1 addition & 1 deletion patterns/eoa-checks/KingOfTheHill.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// A king of the hill game where each new king must pay the old king
// more than they paid. Vulnerable to a DoS attack if the old king
Expand Down
2 changes: 1 addition & 1 deletion patterns/erc20-compatibility/ERC20Compatibility.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// Compliant ERC20 interface: https://eips.ethereum.org/EIPS/eip-20
interface IERC20 {
Expand Down
2 changes: 1 addition & 1 deletion patterns/erc20-permit/PermitSwap.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// Minimal ERC20 interface.
interface IERC20 {
Expand Down
2 changes: 1 addition & 1 deletion patterns/error-handling/PooledExecute.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// Allows strangers to pool their ETH together to attach to a single arbitrary call
// that will be made once enough ETH is raised.
Expand Down
2 changes: 1 addition & 1 deletion patterns/eth_call-tricks/swap-forwarder/Contracts.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// Contract that we will fake deploy and call directly into to evaluate
// the outcome of a complex swap between two protocols.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// Contract that we will fake deploy and call directly into to evaluate
// the outcome of a complex swap between two protocols.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

// A proxy contract that delegatecalls incoming calls to an implementation contract.
// Vulnerable because the implementation contract overlaps the same storage slots
Expand Down
2 changes: 1 addition & 1 deletion patterns/factory-proofs/FactoryProofs.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
pragma solidity ^0.8.23;

contract FactoryProofs {
// Validate that `deployed` was deployed by `deployer` using regular create opcode
Expand Down
74 changes: 74 additions & 0 deletions patterns/flash-loans/FlashLoanPool.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

// Minimal ERC20 interface.
interface IERC20 {
function balanceOf(address owner) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
}

// Interface implemented by a flash loan borrower contract.
interface IBorrower {
function onFlashLoan(
// Who called `flashLoan()`.
address operator,
// Token borrowed.
IERC20 token,
// Amount of tokens borrowed.
uint256 amount,
// Extra tokens (on top of `amount`) to return as the loan fee.
uint256 fee,
// Arbitrary data passed into `flashLoan()`.
bytes calldata data
) external;
}

// A simple flash loan protocol with a single depositor/withdrawer (OWNER).
contract FlashLoanPool {
uint16 public constant FEE_BPS = 0.001e4; // 0.1% fee.
address public immutable OWNER;

constructor(address owner) { OWNER = owner; }

// Perform a flash loan.
function flashLoan(
// Token to borrow.
IERC20 token,
// How much to borrow.
uint256 borrowAmount,
// Address of the borrower (handler) contract.
IBorrower borrower,
// Arbitrary data to pass to borrower contract.
bytes calldata data
)
external
{
// Snapshot our token balance before the transfer.
uint256 balanceBefore = token.balanceOf(address(this));
require(balanceBefore >= borrowAmount, 'too much');
// Compute the fee, rounded up.
uint256 fee = FEE_BPS * (borrowAmount + 1e4-1) / 1e4;
// Transfer tokens to the borrower contract.
token.transfer(address(borrower), borrowAmount);
// Let the borrower do its thing.
borrower.onFlashLoan(
msg.sender,
token,
borrowAmount,
fee,
data
);
// Check that all the tokens were returned + fee.
uint256 balanceAfter = token.balanceOf(address(this));
require(balanceAfter >= balanceBefore + fee, 'not repaid');
}

// Withdraw tokens from this contract to the contract owner.
function withdraw(IERC20 token, uint256 amount)
external
{
require(msg.sender == OWNER, 'not owner');
token.transfer(msg.sender, amount);
}
}
108 changes: 108 additions & 0 deletions patterns/flash-loans/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Flash Loans

- [📜 Example Code](./FlashLoanPool.sol)
- [🐞 Tests](../../test/FlashLoanPool.t.sol)

For better or worse, flash loans are a permanent fixture of the modern defi landscape. As the name implies, flash loans allow people to borrow massive (sometimes protocol breaking) amounts of an asset asset during the lifespan of a function call, typically for just a small (or no) fee. For protocols that custody assets, flash loans can be an additional source of yield without risking any of its assets... if implemented [securely](#security-considerations) 🤞.

Here we'll explore creating a basic flash loan protocol to illustrate the concept.

## Anatomy of a Flash Loan

At their core, flash loans are actually fairly simple, following this typical flow:

1. Transfer loaned assets to a user-provided borrower contract.
2. Call a handler function on the borrower contract to hand over execution control.
1. Let the borrower contract perform whatever actions it needs to do with those assets.
3. After the borrower's handler function returns, verify that all of the borrowed assets have been returned + some extra as fee.


![flash loan flow](./flash-loan-flow.drawio.svg)

The entirety of the loan occurs inside of the call to the loan function. If the borrower fails to return the assets (+ fee) by the time their logic completes, the entire call frame reverts and it will be as if the loan and the actions performed within never happened, exposing no assets to any™️ risk. It's this lack of risk that helps drive the fee associated with flash loans down.

## A Simple FLash Loan Protocol

Let's write a super simple ERC20 pool contract owned and funded by a single entity. Borrowers can come along and take a flash loan against the pool's tokens, earning a small fee along the way and increasing the total value of the pool. For additional simplicity, this contract will only support [compliant](../erc20-compatibility/) ERC20 tokens that don't take fees on transfer.

We're looking at the following minimal interfaces for this protocol:

```solidity
// Interface implemented by our protocol.
interface IFLashLoanPool {
// Perform a flash loan.
function flashLoan(
// Token to borrow.
IERC20 token,
// How much to borrow.
uint256 borrowAmount,
// Address of the borrower (handler) contract.
IBorrower borrower,
// Arbitrary data to pass to borrower contract.
bytes calldata data
) external;
// Withdraw tokens held by this contract to the contract owner.
function withdraw(IERC20 token, uint256 amount) external;
}
// Interface implemented by a flash loan borrower.
interface IBorrower {
function onFlashLoan(
// Who called `flashLoan()`.
address operator,
// Token borrowed.
IERC20 token,
// Amount of tokens borrowed.
uint256 amount,
// Extra tokens (on top of `amount`) to return as the loan fee.
uint256 fee,
// Arbitrary data passed into `flashLoan()`.
bytes calldata data
) external;
}
```

Let's immediately flesh out `flashLoan()`, which is really all we need to have a functioning flash loan protocol. It needs to 1) track the token balances, 2) transfer tokens to the borrower, 3) hand over execution control to the borrower, then 4) verify all the assets were returned. We'll use the constant `FEE_BPS` to define the flash loan fee in BPS (e.g., `1% == 0.01e4`).

```solidity
function flashLoan(
IERC20 token,
uint256 borrowAmount,
IBorrower borrower,
bytes calldata data
) external {
// Snapshot our token balance before the transfer.
uint256 balanceBefore = token.balanceOf(address(this));
require(balanceBefore >= borrowAmount, 'too much');
// Compute the fee, rounded up.
uint256 fee = FEE_BPS * (borrowAmount + 1e4-1) / 1e4;
// Transfer tokens to the borrower contract.
token.transfer(address(borrower), borrowAmount);
// Let the borrower do its thing.
borrower.onFlashLoan(
msg.sender,
token,
borrowAmount,
fee,
data
);
// Check that all the tokens were returned + fee.
uint256 balanceAfter = token.balanceOf(address(this));
require(balanceAfter >= balanceBefore + fee, 'not repaid');
}
```

The `withdraw()` function is trivial to implement so we'll omit it from this guide, but you can see the complete contract [here](./FlashLoanPool.sol).

## Security Considerations

Implementing flash loans might have seemed really simple but usually they're added on top of an existing, more complex product. For example, Aave, Dydx, and Uniswap all have flash loan capabilities added to their lending and exchange products. The transfer-and-call pattern used by flash loans creates a huge opportunity for [reentrancy](../reentrancy/) and price manipulation attacks when in the setting of even a small protocol.

For instance, let's say we took the natural progression of our toy example and allowed anyone to deposit assets, granting them shares that entitles them to a proportion of generated fees. Now we would have to wonder what could happen if the flash loan borrower re-deposited borrowed assets into the pool. Without proper safeguards, it's very possible that we could double count these assets and the borrower would be able to unfairly inflate the number of their own shares and then drain all the assets out of the pool after the flash loan operation!

Extreme care has to be taken any time you do any kind of arbitrary function callback, but especially if there's value associated with it.

## Test Demo: DEX Arbitrage Borrower

Check the [tests](../../test/FlashLoanPool.t.sol) for an illustration of how a user would use our flash loan feature. There you'll find a fun borrower contract designed to perform arbitrary swap operations across different DEXes to capture a zero-capital arbitrage opportunity, with profits split between the operator and fee.
Loading

0 comments on commit 1d725a9

Please sign in to comment.