Skip to content

Instantly share code, notes, and snippets.

@DonGuillotine
Created December 5, 2024 03:05
Show Gist options
  • Save DonGuillotine/10249319de4fa03e748ebfe3f86c53e4 to your computer and use it in GitHub Desktop.
Save DonGuillotine/10249319de4fa03e748ebfe3f86c53e4 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./interfaces/IFlashBorrower.sol";
import "./interfaces/ICauldron.sol";
import "./interfaces/IBentoBox.sol";
import "./interfaces/ILendingPool.sol";
/**
* @title FlashLoanLiquidator
* @notice Implements flash loan liquidation strategy on Fantom network
*/
contract FlashLoanLiquidator is IFlashBorrower, ReentrancyGuard, Ownable {
// Custom errors
error UnauthorizedCaller();
error SlippageExceeded();
error EmergencyStop();
error InvalidAmount();
error OperationFailed();
// Constants
address public immutable MIM;
address public immutable WFTM;
address public immutable FLASHLOAN_PROTOCOL;
address public immutable LENDING_POOL;
address public immutable BENTO_BOX;
address public immutable CAULDRON_FTM;
// State variables
bool public emergencyStop;
mapping(address => uint256) public shareBalances;
uint256 public constant SLIPPAGE_TOLERANCE = 50;
// Events
event FlashLoanExecuted(address asset, uint256 amount);
event CollateralDeposited(address token, uint256 amount, uint256 share);
event WFTMBorrowed(uint256 amount, uint256 share);
event LiquidationExecuted();
event CollateralClaimed(uint256 amount);
event FlashLoanRepaid(uint256 amount, uint256 fee);
// Interfaces
IBentoBox private immutable bentoBox;
ICauldron private immutable cauldron;
ILendingPool private immutable lendingPool;
IERC20 private immutable mimToken;
IERC20 private immutable wftmToken;
constructor(
address _mim,
address _wftm,
address _flashloanProtocol,
address _lendingPool,
address _bentoBox,
address _cauldronFtm
) Ownable(msg.sender) {
MIM = _mim;
WFTM = _wftm;
FLASHLOAN_PROTOCOL = _flashloanProtocol;
LENDING_POOL = _lendingPool;
BENTO_BOX = _bentoBox;
CAULDRON_FTM = _cauldronFtm;
bentoBox = IBentoBox(_bentoBox);
cauldron = ICauldron(_cauldronFtm);
lendingPool = ILendingPool(_lendingPool);
mimToken = IERC20(_mim);
wftmToken = IERC20(_wftm);
bentoBox.registerProtocol();
mimToken.approve(_bentoBox, type(uint256).max);
mimToken.approve(_lendingPool, type(uint256).max);
wftmToken.approve(_bentoBox, type(uint256).max);
}
/**
* @notice Initiates the flash loan liquidation strategy
* @param flashLoanAmount Amount of MIM to borrow in flash loan
* @param minExpectedReturn Minimum expected return in WFTM
*/
function executeLiquidationStrategy(
uint256 flashLoanAmount,
uint256 minExpectedReturn
) external nonReentrant {
if (emergencyStop) revert EmergencyStop();
if (flashLoanAmount == 0) revert InvalidAmount();
// Encode parameters for flash loan callback
bytes memory params = abi.encode(minExpectedReturn);
// Execute flash loan
lendingPool.flashLoan(
this,
address(this),
IERC20(MIM),
flashLoanAmount,
params
);
}
/**
* @notice Flash loan callback handler implementing liquidation strategy
* @dev Called by lending pool after flash loan is provided
*/
function onFlashLoan(
address sender,
IERC20 token,
uint256 amount,
uint256 fee,
bytes calldata data
) external override nonReentrant {
// Verify caller and token
if (msg.sender != LENDING_POOL || sender != address(this)) {
revert UnauthorizedCaller();
}
if (address(token) != MIM) revert InvalidAmount();
// Decode parameters
uint256 minExpectedReturn = abi.decode(data, (uint256));
// Step 1: Deposit MIM into BentoBox
(uint256 depositAmount, uint256 shareOut) = _depositToBentoBox(amount);
emit CollateralDeposited(MIM, depositAmount, shareOut);
// Step 2: Add MIM as collateral in Cauldron
_addCollateralToCauldron(shareOut);
// Step 3: Borrow WFTM from Cauldron
(uint256 borrowAmount, uint256 borrowShare) = _borrowWFTM(amount);
emit WFTMBorrowed(borrowAmount, borrowShare);
// Step 4: Execute liquidation
_executeLiquidation();
emit LiquidationExecuted();
// Step 5: Remove and claim collateral
uint256 claimedAmount = _claimCollateral();
emit CollateralClaimed(claimedAmount);
// Step 6: Withdraw MIM from BentoBox
_withdrawFromBentoBox();
// Verify minimum return
if (claimedAmount < minExpectedReturn) revert SlippageExceeded();
// Repay flash loan
uint256 repayAmount = amount + fee;
require(
mimToken.transfer(LENDING_POOL, repayAmount),
"Flash loan repayment failed"
);
emit FlashLoanRepaid(amount, fee);
}
/**
* @notice Deposits MIM into BentoBox
* @param amount Amount of MIM to deposit
* @return amountOut Amount deposited
* @return shareOut Shares received
*/
function _depositToBentoBox(uint256 amount) private returns (uint256 amountOut, uint256 shareOut) {
(amountOut, shareOut) = bentoBox.deposit(
IERC20(MIM),
address(this),
address(this),
amount,
0
);
shareBalances[MIM] += shareOut;
return (amountOut, shareOut);
}
/**
* @notice Adds MIM collateral to Cauldron
* @param shareAmount Amount of shares to add as collateral
*/
function _addCollateralToCauldron(uint256 shareAmount) private {
cauldron.addCollateral(address(this), false, shareAmount);
}
/**
* @notice Borrows WFTM from Cauldron
* @param collateralAmount Amount of collateral provided
* @return part Borrowed part
* @return share Borrowed share
*/
function _borrowWFTM(uint256 collateralAmount) private returns (uint256 part, uint256 share) {
// Calculate safe borrow amount (e.g., 75% of collateral value)
uint256 borrowAmount = (collateralAmount * 75) / 100;
return cauldron.borrow(address(this), borrowAmount);
}
/**
* @notice Executes the liquidation strategy
*/
function _executeLiquidation() private {
cauldron.liquidate();
}
/**
* @notice Claims collateral after liquidation
* @return amount Amount of collateral claimed
*/
function _claimCollateral() private returns (uint256 amount) {
uint256 share = shareBalances[WFTM];
cauldron.removeCollateral(address(this), share);
shareBalances[WFTM] = 0;
return wftmToken.balanceOf(address(this));
}
/**
* @notice Withdraws MIM from BentoBox
* @return amount Amount withdrawn
*/
function _withdrawFromBentoBox() private returns (uint256 amount) {
uint256 share = shareBalances[MIM];
(amount, ) = bentoBox.withdraw(
IERC20(MIM),
address(this),
address(this),
0,
share
);
shareBalances[MIM] = 0;
return amount;
}
/**
* @notice Emergency withdrawal of tokens
* @param token Address of token to withdraw
*/
function emergencyWithdraw(address token) external onlyOwner {
uint256 balance;
uint256 shareBalance = shareBalances[token];
// If tokens are in BentoBox, withdraw them first
if (shareBalance > 0) {
(uint256 amount, ) = bentoBox.withdraw(
IERC20(token),
address(this),
address(this),
0,
shareBalance
);
shareBalances[token] = 0;
balance = amount;
}
// Transfer any remaining tokens in the contract
balance += IERC20(token).balanceOf(address(this));
if (balance > 0) {
require(
IERC20(token).transfer(owner(), balance),
"Emergency withdrawal failed"
);
}
}
/**
* @notice Toggles emergency stop
*/
function toggleEmergencyStop() external onlyOwner {
emergencyStop = !emergencyStop;
}
/**
* @notice Updates protocol approvals
* @param token Token address
* @param spender Spender address
* @param amount Approval amount
*/
function updateApproval(
address token,
address spender,
uint256 amount
) external onlyOwner {
require(
IERC20(token).approve(spender, amount),
"Approval update failed"
);
}
/**
* @dev Prevents accidental ETH transfers to contract
*/
receive() external payable {
revert("Direct ETH transfers not accepted");
}
/**
* @notice Helper function to check share balances
* @param token Token address
* @return Share balance
*/
function getShareBalance(address token) external view returns (uint256) {
return shareBalances[token];
}
/**
* @notice Helper function to check if emergency stop is active
* @return Emergency stop status
*/
function isEmergencyStop() external view returns (bool) {
return emergencyStop;
}
/**
* @notice Helper function to validate contract setup
* @return isValid Whether contract is properly set up
*/
function validateSetup() external view returns (bool) {
return (
address(bentoBox) == BENTO_BOX &&
address(cauldron) == CAULDRON_FTM &&
address(lendingPool) == LENDING_POOL &&
mimToken.allowance(address(this), BENTO_BOX) > 0 &&
mimToken.allowance(address(this), LENDING_POOL) > 0 &&
wftmToken.allowance(address(this), BENTO_BOX) > 0
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment