diff --git a/src/Blue.sol b/src/Blue.sol index a6fbb19ad..362467739 100644 --- a/src/Blue.sol +++ b/src/Blue.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.21; import {IIrm} from "src/interfaces/IIrm.sol"; import {IERC20} from "src/interfaces/IERC20.sol"; +import {IFlashLender} from "src/interfaces/IFlashLender.sol"; +import {IFlashBorrower} from "src/interfaces/IFlashBorrower.sol"; import {Errors} from "./libraries/Errors.sol"; import {SharesMath} from "src/libraries/SharesMath.sol"; @@ -13,7 +15,7 @@ import {SafeTransferLib} from "src/libraries/SafeTransferLib.sol"; uint256 constant MAX_FEE = 0.25e18; uint256 constant ALPHA = 0.5e18; -contract Blue { +contract Blue is IFlashLender { using SharesMath for uint256; using FixedPointMathLib for uint256; using SafeTransferLib for IERC20; @@ -245,6 +247,17 @@ contract Blue { market.borrowableAsset.safeTransferFrom(msg.sender, address(this), repaid); } + // Flash Loans. + + /// @inheritdoc IFlashLender + function flashLoan(IFlashBorrower receiver, address token, uint256 amount, bytes calldata data) external { + IERC20(token).safeTransfer(address(receiver), amount); + + receiver.onFlashLoan(msg.sender, token, amount, data); + + IERC20(token).safeTransferFrom(address(receiver), address(this), amount); + } + // Position management. function setApproval(address manager, bool isAllowed) external { diff --git a/src/interfaces/IFlashBorrower.sol b/src/interfaces/IFlashBorrower.sol new file mode 100644 index 000000000..eb0d490d6 --- /dev/null +++ b/src/interfaces/IFlashBorrower.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.5.0; + +interface IFlashBorrower { + /// @dev Receives a flash loan. + /// @param initiator The initiator of the loan. + /// @param token The token lent. + /// @param amount The amount of tokens lent. + /// @param data Arbitrary data structure, intended to contain user-defined parameters. + function onFlashLoan(address initiator, address token, uint256 amount, bytes calldata data) external; +} diff --git a/src/interfaces/IFlashLender.sol b/src/interfaces/IFlashLender.sol new file mode 100644 index 000000000..603e18902 --- /dev/null +++ b/src/interfaces/IFlashLender.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.5.0; + +import {IFlashBorrower} from "src/interfaces/IFlashBorrower.sol"; + +interface IFlashLender { + /// @dev Initiate a flash loan. + /// @param receiver The receiver of the tokens in the loan, and the receiver of the callback. + /// @param token The token lent. + /// @param amount The amount of tokens lent. + /// @param data Arbitrary data structure, intended to contain user-defined parameters. + function flashLoan(IFlashBorrower receiver, address token, uint256 amount, bytes calldata data) external; +} diff --git a/src/libraries/Errors.sol b/src/libraries/Errors.sol index 0bc93a645..62c7a9a50 100644 --- a/src/libraries/Errors.sol +++ b/src/libraries/Errors.sol @@ -25,4 +25,6 @@ library Errors { string internal constant INSUFFICIENT_LIQUIDITY = "insufficient liquidity"; string internal constant HEALTHY_POSITION = "position is healthy"; + + string internal constant INVALID_SUCCESS_HASH = "invalid success hash"; } diff --git a/src/mocks/FlashBorrowerMock.sol b/src/mocks/FlashBorrowerMock.sol new file mode 100644 index 000000000..08c5aa869 --- /dev/null +++ b/src/mocks/FlashBorrowerMock.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import {IFlashLender} from "src/interfaces/IFlashLender.sol"; +import {IFlashBorrower} from "src/interfaces/IFlashBorrower.sol"; + +import {ERC20, SafeTransferLib} from "solmate/utils/SafeTransferLib.sol"; + +contract FlashBorrowerMock is IFlashBorrower { + using SafeTransferLib for ERC20; + + IFlashLender private immutable _LENDER; + + constructor(IFlashLender lender) { + _LENDER = lender; + } + + /* EXTERNAL */ + + /// @inheritdoc IFlashBorrower + function onFlashLoan(address, address token, uint256 amount, bytes calldata) external { + require(msg.sender == address(_LENDER), "invalid lender"); + + ERC20(token).safeApprove(address(_LENDER), amount); + } +} diff --git a/test/forge/Blue.t.sol b/test/forge/Blue.t.sol index a7e42ece6..b3d3d0efe 100644 --- a/test/forge/Blue.t.sol +++ b/test/forge/Blue.t.sol @@ -8,6 +8,7 @@ import "src/Blue.sol"; import {ERC20Mock as ERC20} from "src/mocks/ERC20Mock.sol"; import {OracleMock as Oracle} from "src/mocks/OracleMock.sol"; import {IrmMock as Irm} from "src/mocks/IrmMock.sol"; +import {FlashBorrowerMock} from "src/mocks/FlashBorrowerMock.sol"; contract BlueTest is Test { using MarketLib for Market; @@ -26,6 +27,7 @@ contract BlueTest is Test { Irm private irm; Market public market; Id public id; + FlashBorrowerMock internal flashBorrower; function setUp() public { // Create Blue. @@ -36,6 +38,7 @@ contract BlueTest is Test { collateralAsset = new ERC20("collateral", "C", 18); borrowableOracle = new Oracle(); collateralOracle = new Oracle(); + flashBorrower = new FlashBorrowerMock(blue); irm = new Irm(blue); @@ -690,6 +693,17 @@ contract BlueTest is Test { vm.stopPrank(); } + function testFlashLoan(uint256 amount) public { + amount = bound(amount, 1, 2 ** 64); + + borrowableAsset.setBalance(address(this), amount); + blue.supply(market, amount, address(this)); + + blue.flashLoan(flashBorrower, address(borrowableAsset), amount, bytes("")); + + assertEq(borrowableAsset.balanceOf(address(blue)), amount, "balanceOf"); + } + function testExtsLoad(uint256 slot, bytes32 value0) public { bytes32[] memory slots = new bytes32[](2); slots[0] = bytes32(slot); diff --git a/test/hardhat/Blue.spec.ts b/test/hardhat/Blue.spec.ts index 08193f88e..062ca4651 100644 --- a/test/hardhat/Blue.spec.ts +++ b/test/hardhat/Blue.spec.ts @@ -5,6 +5,7 @@ import { expect } from "chai"; import { BigNumber, constants, utils } from "ethers"; import hre from "hardhat"; import { Blue, OracleMock, ERC20Mock, IrmMock } from "types"; +import { FlashBorrowerMock } from "types/src/mocks/FlashBorrowerMock"; const closePositions = false; const initBalance = constants.MaxUint256.div(2); @@ -45,6 +46,7 @@ describe("Blue", () => { let borrowableOracle: OracleMock; let collateralOracle: OracleMock; let irm: IrmMock; + let flashBorrower: FlashBorrowerMock; let market: Market; let id: Buffer; @@ -110,6 +112,10 @@ describe("Blue", () => { await borrowable.setBalance(liquidator.address, initBalance); await borrowable.connect(liquidator).approve(blue.address, constants.MaxUint256); + + const FlashBorrowerFactory = await hre.ethers.getContractFactory("FlashBorrowerMock", admin); + + flashBorrower = await FlashBorrowerFactory.deploy(blue.address); }); it("should simulate gas cost [main]", async () => { @@ -185,4 +191,13 @@ describe("Blue", () => { await borrowableOracle.setPrice(BigNumber.WAD); } }); + + it("should simuate gas cost [flashloan]", async () => { + const user = signers[0]; + const amount = BigNumber.WAD; + + await blue.connect(user).supply(market, amount, user.address); + + await blue.flashLoan(flashBorrower.address, borrowable.address, amount.div(2), []); + }); });