Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip on reentrant calculate orders #40

Merged
merged 23 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 87 additions & 57 deletions .gas-snapshot

Large diffs are not rendered by default.

Binary file modified meta/OrderBook.rain.meta
Binary file not shown.
2 changes: 1 addition & 1 deletion slither.config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"detectors_to_exclude": "assembly-usage",
"detectors_to_exclude": "assembly-usage,solc-version",
"filter_paths": "forge-std,openzeppelin,test"
}
2 changes: 1 addition & 1 deletion src/abstract/OrderBookV3ArbCommon.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: CAL
pragma solidity ^0.8.18;
pragma solidity ^0.8.19;

/// Thrown when the minimum output for the sender is not met after the arb.
/// @param minimum The minimum output expected by the sender.
Expand Down
2 changes: 1 addition & 1 deletion src/abstract/OrderBookV3ArbOrderTaker.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: CAL
pragma solidity =0.8.19;
pragma solidity ^0.8.19;

import {ERC165, IERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol";
import {ReentrancyGuard} from "lib/openzeppelin-contracts/contracts/security/ReentrancyGuard.sol";
Expand Down
23 changes: 15 additions & 8 deletions src/abstract/OrderBookV3FlashBorrower.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: CAL
pragma solidity =0.8.19;
pragma solidity ^0.8.19;

import {ERC165, IERC165} from "lib/openzeppelin-contracts/contracts/utils/introspection/ERC165.sol";
import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
Expand All @@ -12,13 +12,20 @@ import {
DeployerDiscoverableMetaV2ConstructionConfig,
LibMeta
} from "lib/rain.interpreter/src/abstract/DeployerDiscoverableMetaV2.sol";
import "lib/rain.interpreter/src/lib/caller/LibEncodedDispatch.sol";
import "lib/rain.interpreter/src/lib/caller/LibContext.sol";
import "lib/rain.interpreter/src/lib/bytecode/LibBytecode.sol";

import "../interface/unstable/IOrderBookV3.sol";
import "lib/rain.factory/src/interface/ICloneableV2.sol";
import "./OrderBookV3ArbCommon.sol";
import {LibEncodedDispatch, EncodedDispatch} from "lib/rain.interpreter/src/lib/caller/LibEncodedDispatch.sol";
import {LibContext} from "lib/rain.interpreter/src/lib/caller/LibContext.sol";
import {LibBytecode} from "lib/rain.interpreter/src/lib/bytecode/LibBytecode.sol";
import {ON_FLASH_LOAN_CALLBACK_SUCCESS} from "../interface/ierc3156/IERC3156FlashBorrower.sol";
import {EvaluableConfigV2} from "rain.interpreter/src/lib/caller/LibEvaluable.sol";
import {IOrderBookV3, TakeOrdersConfigV2, NoOrders} from "../interface/unstable/IOrderBookV3.sol";
import {ICloneableV2, ICLONEABLE_V2_SUCCESS} from "lib/rain.factory/src/interface/ICloneableV2.sol";
import {
IInterpreterV1, SourceIndex, DEFAULT_STATE_NAMESPACE
} from "lib/rain.interpreter/src/interface/IInterpreterV1.sol";
import {IERC3156FlashBorrower} from "../interface/ierc3156/IERC3156FlashBorrower.sol";
import {IInterpreterStoreV1} from "lib/rain.interpreter/src/interface/IInterpreterStoreV1.sol";
import {BadLender, MinimumOutput, NonZeroBeforeArbStack, Initializing} from "./OrderBookV3ArbCommon.sol";
import {SignedContextV1} from "rain.interpreter/src/interface/IInterpreterCallerV2.sol";

/// Thrown when the initiator is not the order book.
/// @param badInitiator The untrusted initiator of the flash loan.
Expand Down
171 changes: 21 additions & 150 deletions src/abstract/OrderBookV3FlashLender.sol
Original file line number Diff line number Diff line change
@@ -1,33 +1,17 @@
// SPDX-License-Identifier: CAL
pragma solidity ^0.8.18;
pragma solidity ^0.8.19;

import {Math} from "lib/openzeppelin-contracts/contracts/utils/math/Math.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";

import "../interface/ierc3156/IERC3156FlashBorrower.sol";
import "../interface/ierc3156/IERC3156FlashLender.sol";

/// Thrown when `flashLoan` token is zero address.
error ZeroToken();

/// Thrown when `flashLoan` receiver is zero address.
error ZeroReceiver();

/// Thrown when `flashLoan` amount is zero.
error ZeroAmount();
import {IERC3156FlashBorrower, ON_FLASH_LOAN_CALLBACK_SUCCESS} from "../interface/ierc3156/IERC3156FlashBorrower.sol";
import {IERC3156FlashLender} from "../interface/ierc3156/IERC3156FlashLender.sol";

/// Thrown when the `onFlashLoan` callback returns anything other than
/// ON_FLASH_LOAN_CALLBACK_SUCCESS.
/// @param result The value that was returned by `onFlashLoan`.
error FlashLenderCallbackFailed(bytes32 result);

/// Thrown when more than one debt is attempted simultaneously.
/// @param receiver The receiver of the active debt.
/// @param token The token of the active debt.
/// @param amount The amount of the active debt.
error ActiveDebt(address receiver, address token, uint256 amount);

/// @dev Flash fee is always 0 for orderbook as there's no entity to take
/// revenue for `Orderbook` and its more important anyway that flashloans happen
/// to connect external liquidity to live orders via arbitrage.
Expand All @@ -36,151 +20,38 @@ uint256 constant FLASH_FEE = 0;
/// @title OrderBookV3FlashLender
/// @notice Implements `IERC3156FlashLender` for `OrderBook`. Based on the
/// reference implementation by Alberto Cuesta Cañada found at
/// https://eips.ethereum.org/EIPS/eip-3156
/// Several features found in the reference implementation are simplified or
/// hardcoded for `OrderBookV3`.
/// https://eips.ethereum.org/EIPS/eip-3156#flash-loan-reference-implementation
abstract contract OrderBookV3FlashLender is IERC3156FlashLender {
using Math for uint256;
using SafeERC20 for IERC20;

IERC3156FlashBorrower private _sReceiver = IERC3156FlashBorrower(address(0));
address private _sToken = address(0);
uint256 private _sAmountUnpaid = 0;

/// If any of the debt related storage values are set then we consider a
/// debt active. Notably the debt is still active even if the amount unpaid
/// is zero, until the loan originating `flashLoan` call fully completes.
function _isActiveDebt() internal view returns (bool) {
return (address(_sReceiver) != address(0)) || (_sToken != address(0)) || (_sAmountUnpaid != 0);
}

function _checkActiveDebt() internal view {
if (_isActiveDebt()) {
revert ActiveDebt(address(_sReceiver), _sToken, _sAmountUnpaid);
}
}

/// Whenever `Orderbook` sends tokens to any address it MUST first attempt
/// to decrease any outstanding flash loans for that address. Consider the
/// case that Alice deposits 100 TKN and she is the only depositor of TKN
/// then flash borrows 100 TKN. If she attempts to withdraw 100 TKN during
/// her `onFlashLoan` callback then `Orderbook`:
///
/// - has 0 TKN balance to process the withdrawal
/// - MUST process the withdrawal as Alice has the right to withdraw her
/// balance at any time
/// - Has the 100 TKN debt active under Alice
///
/// In this case `Orderbook` can simply forgive Alice's 100 TKN debt instead
/// of actually transferring any tokens. The withdrawal can decrease her
/// vault balance by 100 TKN decoupled from needing to know whether a
/// tranfer or forgiveness happened.
///
/// The same logic applies to withdrawals as sending tokens during
/// `takeOrders` as the reason for sending tokens is irrelevant, all that
/// matters is that `Orderbook` prioritises debt repayments over external
/// transfers.
///
/// If there is an active debt that only partially eclipses the withdrawal
/// then the debt will be fully repaid and the remainder transferred as a
/// real token transfer.
///
/// Note that Alice can still contrive a situation that causes `Orderbook`
/// to attempt to send tokens that it does not have. If Alice can write a
/// smart contract to trigger withdrawals she can flash loan 100% of the
/// TKN supply in `Orderbook` and trigger her contract to attempt a
/// withdrawal. For any normal ERC20 token this will fail and revert as the
/// `Orderbook` cannot send tokens it does not have under any circumstances,
/// but the scenario is worth being aware of for more exotic token
/// behaviours that may not be supported.
///
/// @param token The token being sent or for the debt being paid.
/// @param receiver The receiver of the token or holder of the debt.
/// @param sendAmount The amount to send or repay.
/// @return The final amount sent after any debt repayment.
function _decreaseFlashDebtThenSendToken(address token, address receiver, uint256 sendAmount)
internal
returns (uint256)
{
// If this token transfer matches the active debt then prioritise
// reducing debt over sending tokens.
if (token == _sToken && receiver == address(_sReceiver)) {
uint256 debtReduction = sendAmount.min(_sAmountUnpaid);
sendAmount -= debtReduction;

// Even if this completely zeros the amount the debt is considered
// active until the `flashLoan` also clears the token and recipient.
_sAmountUnpaid -= debtReduction;
}

if (sendAmount > 0) {
IERC20(token).safeTransfer(receiver, sendAmount);
}
return sendAmount;
}

/// @inheritdoc IERC3156FlashLender
function flashLoan(IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data)
external
override
returns (bool)
{
// Set the active debt before transferring tokens to prevent reeentrancy.
// The active debt is set beyond the scope of `flashLoan` to facilitate
// early repayment via. `_decreaseFlashDebtThenSendToken`.
{
if (address(receiver) == address(0)) {
revert ZeroReceiver();
}
if (token == address(0)) {
revert ZeroToken();
}
if (amount == 0) {
revert ZeroAmount();
}

// This prevents reentrancy, loans can be taken sequentially within
// a transaction but not simultanously. If any of these values are
// nonzero in storage, they cannot be set to a new value.
_checkActiveDebt();
_sToken = token;
_sReceiver = receiver;
// As long as FLASH_FEE is 0 this `+` will probably get optimised
// away by the compiler.
_sAmountUnpaid = amount + FLASH_FEE;
IERC20(token).safeTransfer(address(receiver), amount);
}
IERC20(token).safeTransfer(address(receiver), amount);

bytes32 result = receiver.onFlashLoan(msg.sender, token, amount, FLASH_FEE, data);
if (result != ON_FLASH_LOAN_CALLBACK_SUCCESS) {
revert FlashLenderCallbackFailed(result);
}

// Pull tokens before releasing the active debt to prevent a new loan
// from being taken reentrantly during the repayment of the current loan.
{
// Sync local `amount_` with global `_amount` in case an early
// repayment was made during the loan term via.
// `_decreaseFlashDebtThenSendToken`.
amount = _sAmountUnpaid;
if (amount > 0) {
// There is no way to fix this slither warning and be compatible
// with ERC3156.
// https://github.com/crytic/slither/issues/1658
//slither-disable-next-line arbitrary-send-erc20
IERC20(token).safeTransferFrom(address(receiver), address(this), amount);
_sAmountUnpaid = 0;
}

// Both of these are required to fully clear the active debt and
// allow new debts.
_sReceiver = IERC3156FlashBorrower(address(0));
_sToken = address(0);
}

// Guard against some bad code path that allowed an active debt to remain
// at this point. Should be impossible.
_checkActiveDebt();
// This behaviour is copied almost verbatim from the ERC3156 spec.
// Slither is complaining because this kind of logic can normally be used
// to grief the token holder. Consider if alice were to approve order book
// for the sake of depositing and then bob could cause alice to send
// tokens to order book without their consent. However, in this case the
// flash loan spec provides two reasons that this is not a problem:
// - We just sent this exact amount to the receiver as the loan, so
// transferring them back with a 0 fee is net neutral.
// - The receiver is a contract that has explicitly opted in to this
// behaviour by implementing `IERC3156FlashBorrower`. The success check
// for `onFlashLoan` guarantees the receiver has opted into this
// behaviour independently of any approvals, etc.
// https://github.com/crytic/slither/issues/1658
//slither-disable-next-line arbitrary-send-erc20
IERC20(token).safeTransferFrom(address(receiver), address(this), amount + FLASH_FEE);

return true;
}
Expand All @@ -195,6 +66,6 @@ abstract contract OrderBookV3FlashLender is IERC3156FlashLender {
/// then loans are disabled so the max becomes `0` until after repayment.
/// @inheritdoc IERC3156FlashLender
function maxFlashLoan(address token) external view override returns (uint256) {
return _isActiveDebt() ? 0 : IERC20(token).balanceOf(address(this));
return IERC20(token).balanceOf(address(this));
}
}
Loading
Loading