Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/brave-lions-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/protocol": patch
---

✨ adapter: use morpho flash loans
111 changes: 63 additions & 48 deletions .gas-snapshot

Large diffs are not rendered by default.

59 changes: 50 additions & 9 deletions contracts/PriceFeedInverse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,70 @@ pragma solidity ^0.8.17;

import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol";
import { SafeCastLib } from "solady/src/utils/SafeCastLib.sol";

import { IPriceFeed } from "./utils/IPriceFeed.sol";

contract PriceFeedInverse is IPriceFeed {
using FixedPointMathLib for uint256;
using SafeCastLib for int256;

/// @notice Price feed whose answer is inverted.
IPriceFeed public immutable priceFeed;
/// @notice Number of decimals that the answer of this price feed has, same as the inverted feed's.
/// @notice Source price feed whose answer is inverted.
IAggregatorProxy public immutable priceFeed;
/// @notice Number of decimals that the answer of this price feed has, same as the source feed's.
uint8 public immutable decimals;
/// @notice Base unit of the inverted feed's answer.
uint256 public immutable baseUnit;
/// @notice Square of the source feed's base unit, used as the numerator to invert its answer.
uint256 public immutable baseUnitSquared;

constructor(IPriceFeed priceFeed_) {
constructor(IAggregatorProxy priceFeed_) {
priceFeed = priceFeed_;
decimals = priceFeed_.decimals();
baseUnit = 10 ** decimals;
baseUnitSquared = 10 ** (2 * uint256(decimals));
}

/// @notice Returns the inverse of the price feed's latest answer.
/// @notice Returns the inverse of the source feed's latest answer.
/// @dev Rounds up, the conservative direction for debt-only markets, so the inverse can't truncate to zero.
/// Reverts on a zero or negative answer.
function latestAnswer() external view returns (int256) {
return int256(baseUnit.mulDivUp(baseUnit, priceFeed.latestAnswer().toUint256()));
return inverse(priceFeed.latestAnswer());
}

/// @notice Returns the source feed's data for `roundId_`, with the answer inverted like `latestAnswer`.
/// @dev A zero answer, which the source returns for rounds without data, is kept as zero.
function getRoundData(
uint80 roundId_
)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
(roundId, answer, startedAt, updatedAt, answeredInRound) = priceFeed.getRoundData(roundId_);
if (answer != 0) answer = inverse(answer);
}

/// @notice Returns the source feed's latest round data, with the answer inverted like `latestAnswer`.
function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
{
(roundId, answer, startedAt, updatedAt, answeredInRound) = priceFeed.latestRoundData();
answer = inverse(answer);
}

/// @notice Returns the source feed's current phase id.
function phaseId() external view returns (uint16) {
return priceFeed.phaseId();
}

function inverse(int256 answer) internal view returns (int256) {
return int256(baseUnitSquared.divUp(answer.toUint256()));
}
}

interface IAggregatorProxy is IPriceFeed {
function getRoundData(uint80 roundId) external view returns (uint80, int256, uint256, uint256, uint80);

function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80);

function phaseId() external view returns (uint16);
}
120 changes: 39 additions & 81 deletions contracts/periphery/FlashLoanAdapter.sol
Original file line number Diff line number Diff line change
@@ -1,108 +1,66 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;

import { AccessControl } from "@openzeppelin/contracts-v4/access/AccessControl.sol";
import { IERC20 } from "@openzeppelin/contracts-v4/interfaces/IERC20.sol";
import { IERC4626 } from "@openzeppelin/contracts-v4/interfaces/IERC4626.sol";
import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol";

import { IFlashLoaner, IFlashLoanRecipient } from "./DebtRoller.sol";

/// @title FlashLoanAdapter
/// @notice Adapter to use Balancer V3 vault with Balancer V2 interface.
/// Uses wrapped tokens when there's not enough liquidity in the vault.
contract FlashLoanAdapter is AccessControl {
IBalancerVaultV3 public immutable vault;
/// @notice Adapter to use Morpho flash loans with the `IFlashLoaner` interface.
/// Morpho flash loans are free, so reported fees are always zero.
contract FlashLoanAdapter is IFlashLoaner {
using SafeTransferLib for address;

/// @notice Mapping to check if a token is a wrapped token.
mapping(IERC20 wToken => bool isWToken) public isWToken;
/// @notice Mapping to get the wrapped token for an asset.
mapping(IERC20 asset => IERC4626 wToken) public wTokens;
/// @notice Morpho contract that provides the flash loans.
IMorpho public immutable morpho;

constructor(IBalancerVaultV3 vault_, address owner) {
_grantRole(DEFAULT_ADMIN_ROLE, owner);
vault = vault_;
constructor(IMorpho morpho_) {
morpho = morpho_;
}

/// @notice Performs a flash loan using the Balancer V3 vault.
/// @notice Performs a flash loan using Morpho.
/// @param recipient The address to receive the flash loan.
/// @param tokens The tokens to borrow.
/// @param amounts The amounts to borrow.
/// @param data Additional data to pass to the recipient.
// solhint-disable-next-line gas-calldata-parameters
function flashLoan(address recipient, IERC20[] memory tokens, uint256[] memory amounts, bytes memory data) external {
function flashLoan(
address recipient,
IERC20[] calldata tokens,
uint256[] calldata amounts,
bytes calldata data
) external {
if (tokens.length != 1 || amounts.length != 1) revert InvalidLength();
uint256[] memory fees = new uint256[](1);
if (tokens[0].balanceOf(address(vault)) < amounts[0]) {
IERC4626 wToken = wTokens[tokens[0]];
if (wToken == IERC4626(address(0)) || wToken.convertToAssets(wToken.balanceOf(address(vault))) < amounts[0]) {
revert InsufficientLiquidity();
}

tokens[0] = IERC20(address(wToken));
uint256 shares = wToken.previewWithdraw(amounts[0]);
fees[0] = wToken.previewMint(shares) - amounts[0];
amounts[0] = shares;
}
vault.unlock(
abi.encodeWithSelector(this.receiveFlashLoan.selector, abi.encode(recipient, tokens, amounts, fees, data))
);
morpho.flashLoan(address(tokens[0]), amounts[0], abi.encode(recipient, tokens[0], data));
}

/// @notice Receives a flash loan from the Balancer V3 vault.
/// @param payload The payload containing the recipient, tokens, amounts, fees, and data.
function receiveFlashLoan(bytes calldata payload) external {
if (msg.sender != address(vault)) revert UnauthorizedVault();
(
address recipient,
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory fees,
bytes memory data
) = abi.decode(payload, (address, IERC20[], uint256[], uint256[], bytes));
/// @notice Receives a flash loan from Morpho, forwards it to the recipient and approves the repayment.
/// @param assets The amount of assets that was flash loaned.
/// @param payload The payload containing the recipient, token, and data.
function onMorphoFlashLoan(uint256 assets, bytes calldata payload) external {
if (msg.sender != address(morpho)) revert UnauthorizedMorpho();
(address recipient, IERC20 token, bytes memory data) = abi.decode(payload, (address, IERC20, bytes));
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = token;
uint256[] memory amounts = new uint256[](1);
amounts[0] = assets;

if (isWToken[tokens[0]]) {
IERC4626 wToken = IERC4626(address(tokens[0]));
vault.sendTo(tokens[0], address(this), amounts[0]);
tokens[0] = IERC20(wToken.asset());
amounts[0] = wToken.redeem(amounts[0], recipient, address(this));
IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, fees, data);
address(token).safeTransfer(recipient, assets);
IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, new uint256[](1), data);

tokens[0].approve(address(wToken), amounts[0] + fees[0]);
wToken.deposit(amounts[0] + fees[0], address(vault));
vault.settle(wToken, amounts[0]);
} else {
vault.sendTo(tokens[0], recipient, amounts[0]);
IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, fees, data);
tokens[0].transfer(address(vault), amounts[0]);
vault.settle(tokens[0], amounts[0]);
}
address(token).safeApprove(address(morpho), assets);
}

/// @notice Sets a wrapped token for an asset.
/// @param asset The asset to set the wrapped token for.
/// @param token The wrapped token to set.
function setWToken(IERC20 asset, IERC4626 token) external onlyRole(DEFAULT_ADMIN_ROLE) {
wTokens[asset] = token;
isWToken[token] = true;
emit WTokenSet(asset, token, msg.sender);
/// @notice Returns the amount of `token` available to be flash loaned.
/// @param token The token to check.
function available(IERC20 token) external view returns (uint256) {
return token.balanceOf(address(morpho));
}
}

interface IBalancerVaultV3 {
function sendTo(IERC20 token, address to, uint256 amount) external;
function settle(IERC20 token, uint256 amountHint) external returns (uint256 credit);
function unlock(bytes calldata data) external returns (bytes memory);
interface IMorpho {
function flashLoan(address token, uint256 assets, bytes calldata data) external;
}

interface IFlashLoanRecipient {
function receiveFlashLoan(
IERC20[] memory tokens,
uint256[] memory amounts,
uint256[] memory fees,
bytes calldata data
) external;
}

error InsufficientLiquidity();
error InvalidLength();
error UnauthorizedVault();

event WTokenSet(IERC20 indexed asset, IERC4626 indexed wToken, address indexed account);
error UnauthorizedMorpho();
12 changes: 3 additions & 9 deletions deploy/FlashLoan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,12 @@ import type { DeployFunction } from "hardhat-deploy/types";
import tenderlify from "./.utils/tenderlify";

const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAccounts }) => {
const [{ address: flashLoaner }, { address: timelock }, { deployer }] = await Promise.all([
get("Balancer3Vault"),
get("TimelockController"),
getNamedAccounts(),
]);
const [{ address: morpho }, { deployer }] = await Promise.all([get("Morpho"), getNamedAccounts()]);

await tenderlify(
"FlashLoanAdapter",
await deploy("FlashLoanAdapter", {
args: [flashLoaner, timelock],
args: [morpho],
skipIfAlreadyDeployed: !JSON.parse(env[`DEPLOY_FLASH_LOAN`] ?? "false"),
from: deployer,
log: true,
Expand All @@ -21,8 +17,6 @@ const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAcco
};

func.tags = ["FlashLoan"];
func.dependencies = ["Governance"];
func.skip = async ({ network, deployments }) =>
!!network.config.sunset || !(await deployments.getOrNull("Balancer3Vault"));
func.skip = async ({ network, deployments }) => !!network.config.sunset || !(await deployments.getOrNull("Morpho"));

export default func;
3 changes: 0 additions & 3 deletions deployments/base/Balancer3Vault.json

This file was deleted.

3 changes: 3 additions & 0 deletions deployments/base/Morpho.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"address": "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb"
}
3 changes: 3 additions & 0 deletions deployments/optimism/Morpho.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"address": "0xce95AfbB8EA029495c66020883F87aaE8864AF92"
}
Loading
Loading