Skip to content
Open
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
71 changes: 44 additions & 27 deletions contracts/gelato/CapInterestHarvester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ import { ILender } from "../interfaces/ILender.sol";
import { IMinter } from "../interfaces/IMinter.sol";
import { IVault } from "../interfaces/IVault.sol";
import { CapInterestHarvesterStorageUtils } from "../storage/CapInterestHarvesterStorageUtils.sol";
import { FlashLoanTransientState } from "./FlashLoanTansientState.sol";
import { IBalancerVault } from "./interfaces/IBalancerVault.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title Cap Interest Harvester
/// @author weso, Cap Labs
/// @notice Harvests interest from borrow and the fractional reserve, sends to fee auction, buys interest, calls distribute on fee receiver
contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access, CapInterestHarvesterStorageUtils {
contract CapInterestHarvester is
ICapInterestHarvester,
UUPSUpgradeable,
Access,
CapInterestHarvesterStorageUtils,
FlashLoanTransientState
{
using SafeERC20 for IERC20;

error InvalidFlashLoan();
Expand Down Expand Up @@ -73,8 +80,6 @@ contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access,
/// 4. Call distribute on fee receiver
_distributeInterest($.feeReceiver);

$.lastharvest = block.timestamp;

emit HarvestedInterest(block.timestamp);
}

Expand All @@ -96,10 +101,7 @@ contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access,

/// @dev Flashloan buy all the interest
/// @param _asset Asset address
function _flashloanBuyInterest(address _balancerVault, address _cusd, address _feeAuction, address _asset)
private
{
CapInterestHarvesterStorage storage $ = getCapInterestHarvesterStorage();
function _flashloanBuyInterest(address _balancerVault, address _cusd, address _feeAuction, address _asset) private {
uint256 assetBalOfFeeAuction = IERC20(_asset).balanceOf(_feeAuction);
uint256 price = IFeeAuction(_feeAuction).currentPrice();
(uint256 cusdAmountFromMint,) = IMinter(_cusd).getMintAmount(_asset, assetBalOfFeeAuction);
Expand All @@ -111,10 +113,9 @@ contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access,
uint256[] memory amounts = new uint256[](1);
amounts[0] = assetBalOfFeeAuction;

IBalancerVault balancerVault = IBalancerVault(_balancerVault);
$.flashInProgress = true;
setFlashLoanInProgress(true);

balancerVault.flashLoan(address(this), assets, amounts, "");
IBalancerVault(_balancerVault).flashLoan(address(this), assets, amounts, "");
}
}

Expand All @@ -132,7 +133,7 @@ contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access,
checkAccess(this.receiveFlashLoan.selector)
{
CapInterestHarvesterStorage storage $ = getCapInterestHarvesterStorage();
if (!$.flashInProgress) revert InvalidFlashLoan();
if (!isFlashLoanInProgress()) revert InvalidFlashLoan();
_checkApproval($.asset, $.cusd);
_checkApproval($.cusd, $.feeAuction);

Expand All @@ -157,7 +158,6 @@ contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access,
IERC20($.asset).safeTransfer($.balancerVault, amounts[0] + feeAmounts[0]);
uint256 excessAmount = IERC20($.asset).balanceOf(address(this));
if (excessAmount > 0) IERC20($.asset).safeTransfer($.excessReceiver, excessAmount);
$.flashInProgress = false;
}

/// @dev Check approval
Expand All @@ -170,11 +170,6 @@ contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access,
}
}

/// @inheritdoc ICapInterestHarvester
function lastHarvest() public view returns (uint256) {
return getCapInterestHarvesterStorage().lastharvest;
}

/// @inheritdoc ICapInterestHarvester
function setExcessReceiver(address _excessReceiver) external checkAccess(this.setExcessReceiver.selector) {
CapInterestHarvesterStorage storage $ = getCapInterestHarvesterStorage();
Expand All @@ -184,23 +179,45 @@ contract CapInterestHarvester is ICapInterestHarvester, UUPSUpgradeable, Access,
}

/// @inheritdoc ICapInterestHarvester
function checker() external view returns (bool canExec, bytes memory execPayload) {
function expectedProfit(uint256 transactionCost) external view returns (int256) {
CapInterestHarvesterStorage storage $ = getCapInterestHarvesterStorage();

// Just harvest if its been 24 hours since last harvest
if (block.timestamp - $.lastharvest > 24 hours) {
return (true, abi.encodeCall(this.harvestInterest, ()));
}

uint256 assetBalOfFeeAuction = IERC20($.asset).balanceOf($.feeAuction);
uint256 amountIn = transactionCost > assetBalOfFeeAuction ? 0 : assetBalOfFeeAuction - transactionCost;
uint256 price = IFeeAuction($.feeAuction).currentPrice();
(uint256 cusdAmountFromMint,) = IMinter($.cusd).getMintAmount($.asset, assetBalOfFeeAuction);
(uint256 cusdAmountFromMint,) = IMinter($.cusd).getMintAmount($.asset, amountIn);

return int256(cusdAmountFromMint) - int256(price);
}

canExec = cusdAmountFromMint > price;
/// @inheritdoc ICapInterestHarvester
function nextProfitable(uint256 secondsPerBlock, uint256 transactionCost) external view returns (uint256, uint256) {
CapInterestHarvesterStorage storage $ = getCapInterestHarvesterStorage();

if (!canExec) return (canExec, bytes("Not enough cUSD to mint"));
uint256 nextProfitableTimestamp;
{
uint256 assetBalOfFeeAuction = IERC20($.asset).balanceOf($.feeAuction);
uint256 amountIn = transactionCost > assetBalOfFeeAuction ? 0 : assetBalOfFeeAuction - transactionCost;
(uint256 cusdAmountFromMint,) = IMinter($.cusd).getMintAmount($.asset, amountIn);

uint256 startPrice = IFeeAuction($.feeAuction).startPrice();
uint256 startTimestamp = IFeeAuction($.feeAuction).startTimestamp();
uint256 duration = IFeeAuction($.feeAuction).duration();

// Profitable when price < cusdAmountFromMint. Solve for elapsed.
// price(elapsed) = startPrice * (1e27 - (elapsed * 0.9e27 / duration)) / 1e27 < cusdAmountFromMint
// => elapsed > (1e27 - cusdAmountFromMint * 1e27 / startPrice) * duration / 0.9e27
// Price floors at 0.1 * startPrice after duration; if cusdAmountFromMint < that, never profitable.
uint256 term = 1e27 - (cusdAmountFromMint * 1e27 / startPrice);
uint256 elapsedNeeded = (term * duration + 0.9e27 - 1) / 0.9e27; // ceiling
if (elapsedNeeded >= duration) return (0, type(uint256).max);

nextProfitableTimestamp = startTimestamp + elapsedNeeded;
if (nextProfitableTimestamp <= block.timestamp) return (block.number, block.timestamp); // already profitable
}

execPayload = abi.encodeCall(this.harvestInterest, ());
uint256 blockOffset = (nextProfitableTimestamp - block.timestamp + secondsPerBlock - 1) / secondsPerBlock;
return (uint256(block.number) + blockOffset, nextProfitableTimestamp);
}

/// @inheritdoc UUPSUpgradeable
Expand Down
23 changes: 23 additions & 0 deletions contracts/gelato/FlashLoanTansientState.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import { TransientSlot } from "@openzeppelin/contracts/utils/TransientSlot.sol";

/// @title Flash Loan Transient State
/// @author weso, Cap Labs
/// @notice Transient state for flash loan
contract FlashLoanTransientState {
using TransientSlot for *;

/// @dev keccak256(abi.encode(uint256(keccak256("cap.storage.FlashLoanTransientState")) - 1)) & ~bytes32(uint256(0xff))
bytes32 internal constant FLASH_LOAN_TRANSIENT_STATE_SLOT =
0x080a6eb1727523b7cca4b7cf7a3debc9ba08c2ed83fc97b4f9cb68f53138c400;

function setFlashLoanInProgress(bool _flashLoanInProgress) internal {
FLASH_LOAN_TRANSIENT_STATE_SLOT.asBoolean().tstore(_flashLoanInProgress);
}

function isFlashLoanInProgress() internal view returns (bool) {
return FLASH_LOAN_TRANSIENT_STATE_SLOT.asBoolean().tload();
}
}
20 changes: 12 additions & 8 deletions contracts/interfaces/ICapInterestHarvester.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ interface ICapInterestHarvester {
address lender;
address balancerVault;
address excessReceiver;
uint256 lastharvest;
bool flashInProgress;
}

/// @notice Initialize the CapInterestHarvester contract
Expand Down Expand Up @@ -59,11 +57,17 @@ interface ICapInterestHarvester {
bytes memory _userData
) external;

/// @notice Last harvest timestamp
function lastHarvest() external view returns (uint256);
/// @notice Expected profit from harvesting (CUSD from minting fee-asset minus auction price).
/// @param transactionCost Cost in asset terms deducted from the fee auction balance before mint
/// @return expectedHarvestProfit Profit in CUSD terms (can be negative)
function expectedProfit(uint256 transactionCost) external view returns (int256 expectedHarvestProfit);

/// @notice Gelato checker function
/// @return canExec Whether the task can be executed
/// @return execPayload The payload to execute
function checker() external view returns (bool canExec, bytes memory execPayload);
/// @notice First block at which harvesting is profitable, or sentinel values.
/// @param secondsPerBlock Estimated seconds per block for block-offset calculation
/// @param transactionCost Cost in asset terms deducted from the fee auction balance before mint
/// @return nextProfitableBlock Block number when profitable; -1 if already profitable; 0 if now or never profitable
function nextProfitable(uint256 secondsPerBlock, uint256 transactionCost)
external
view
returns (uint256 nextProfitableBlock, uint256 nextProfitableTimestamp);
}
11 changes: 5 additions & 6 deletions test/noTest/GelatoInterestHarvester.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,17 @@ contract HarvesterTest is Test {
vm.stopPrank();

vm.prank(admin);
AccessControl(accessControl).grantAccess(
ICapInterestHarvester.receiveFlashLoan.selector, address(proxy), balancerVault
);
AccessControl(accessControl)
.grantAccess(ICapInterestHarvester.receiveFlashLoan.selector, address(proxy), balancerVault);
vm.stopPrank();
}

function test_gelatoHarvest() public {
(bool canExec,) = CapInterestHarvester(address(proxy)).checker();
console.log("canExec", canExec);
int256 expectedProfit = ICapInterestHarvester(address(proxy)).expectedProfit(1);
console.log("expectedProfit", int256(expectedProfit));

vm.prank(gelato);
if (canExec) ICapInterestHarvester(address(proxy)).harvestInterest();
if (expectedProfit > 0) ICapInterestHarvester(address(proxy)).harvestInterest();
vm.stopPrank();
}
}