Skip to content
Draft
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
8 changes: 8 additions & 0 deletions contracts/delegation/Delegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ contract Delegation is IDelegation, UUPSUpgradeable, Access, DelegationStorageUt
return getDelegationStorage().feeRecipient;
}

/// @inheritdoc IDelegation
function collateralAddress(address _agent) external view returns (address) {
DelegationStorage storage $ = getDelegationStorage();
address network = $.agentData[_agent].network;
if (network == address(0)) return address(0);
else return ISymbioticNetworkMiddleware(network).collateralAddress(_agent);
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal override checkAccess(bytes4(0)) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,9 @@ contract SymbioticNetworkMiddleware is
return (IBurnerRouter(address(0)), 0, 0);
}

address collateralAddress = IVault(_vault).collateral();
decimals = IERC20Metadata(collateralAddress).decimals();
(collateralPrice,) = IOracle(_oracle).getPrice(collateralAddress);
address collateral = IVault(_vault).collateral();
decimals = IERC20Metadata(collateral).decimals();
(collateralPrice,) = IOracle(_oracle).getPrice(collateral);
}

/// @dev Verify a vault has the required specifications
Expand Down Expand Up @@ -273,6 +273,15 @@ contract SymbioticNetworkMiddleware is
}
}

/// @notice Get the collateral address of an agent
/// @param _agent Agent address
/// @return collateral The collateral address of the agent
function collateralAddress(address _agent) external view returns (address) {
SymbioticNetworkMiddlewareStorage storage $ = getSymbioticNetworkMiddlewareStorage();
if ($.agentsToVault[_agent] == address(0)) return address(0);
else return IVault($.agentsToVault[_agent]).collateral();
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal override checkAccess(bytes4(0)) { }
}
153 changes: 153 additions & 0 deletions contracts/gelato/CapLiquidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import { Access } from "../access/Access.sol";
import { ICapLiquidator } from "../interfaces/ICapLiquidator.sol";

import { IDelegation } from "../interfaces/IDelegation.sol";
import { ILender } from "../interfaces/ILender.sol";
import { CapLiquidatorStorageUtils } from "../storage/CapLiquidatorStorageUtils.sol";
import { IBalancerVault } from "./interfaces/IBalancerVault.sol";
import { ISwapRouter } from "./interfaces/ISwapRouter.sol";
import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/// @title Cap Liquidator
/// @author kexley, Cap Labs
/// @notice Liquidates assets
contract CapLiquidator is ICapLiquidator, UUPSUpgradeable, Access, CapLiquidatorStorageUtils {
using SafeERC20 for IERC20;

/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}

/// @inheritdoc ICapLiquidator
function initialize(
address _accessControl,
address _lender,
address _delegation,
address _balancerVault,
address _excessReceiver,
address _router
) external initializer {
__Access_init(_accessControl);
__UUPSUpgradeable_init();

CapLiquidatorStorage storage s = getCapLiquidatorStorage();
s.lender = _lender;
s.delegation = _delegation;
s.balancerVault = _balancerVault;
s.excessReceiver = _excessReceiver;
s.router = _router;
}

/// @inheritdoc ICapLiquidator
function liquidate(address _agent, address _asset, uint256 _amount) external {
CapLiquidatorStorage storage $ = getCapLiquidatorStorage();

// Can only borrow up to the balance of the balancer vault
uint256 maxBorrowable = IERC20(_asset).balanceOf($.balancerVault);

address[] memory assets = new address[](1);
uint256[] memory amounts = new uint256[](1);
assets[0] = _asset;
amounts[0] = _amount > maxBorrowable ? maxBorrowable : _amount;

// Set flashInProgress to true to prevent other contracts from initiating a flashloan to this contract
$.flashInProgress = true;

// Flashloan the asset
IBalancerVault($.balancerVault).flashLoan(address(this), assets, amounts, abi.encode(_agent));
}

/// @inheritdoc ICapLiquidator
function receiveFlashLoan(
address[] memory assets,
uint256[] memory amounts,
uint256[] memory feeAmounts,
bytes memory userData
) external {
CapLiquidatorStorage storage $ = getCapLiquidatorStorage();
if (msg.sender != $.balancerVault || !$.flashInProgress) revert InvalidFlashLoan();

address asset = assets[0];
address agent = abi.decode(userData, (address));
address collateral = IDelegation($.delegation).collateralAddress(agent);
if (collateral == address(0)) revert InvalidCollateral();

// Liquidate the asset
_checkApproval(asset, $.lender, amounts[0]);
ILender($.lender).liquidate(agent, asset, amounts[0], 0);

// Swap collateral to asset
uint256 repayAmount = amounts[0] + feeAmounts[0];
uint256 assetBalance = IERC20(asset).balanceOf(address(this));
// Make sure we don't underflow if there is already some asset in the contract
if (repayAmount > assetBalance) {
uint256 swapAmountTo = repayAmount - assetBalance;
uint256 collateralBalance = IERC20(collateral).balanceOf(address(this));
_checkApproval(collateral, $.router, collateralBalance);
ISwapRouter($.router).swapExactOut(collateral, asset, swapAmountTo);
}

// Repay the flashloan
IERC20(asset).safeTransfer($.balancerVault, repayAmount);

// Send excess asset and collateral to the excess receiver
uint256 excessAmount = IERC20(asset).balanceOf(address(this));
if (excessAmount > 0) IERC20(asset).safeTransfer($.excessReceiver, excessAmount);

excessAmount = IERC20(collateral).balanceOf(address(this));
if (excessAmount > 0) IERC20(collateral).safeTransfer($.excessReceiver, excessAmount);

// Unlock the flashloan lock
$.flashInProgress = false;

emit Liquidated(agent, asset, collateral, excessAmount);
}

/// @inheritdoc ICapLiquidator
function setExcessReceiver(address _excessReceiver) external checkAccess(this.setExcessReceiver.selector) {
CapLiquidatorStorage storage $ = getCapLiquidatorStorage();
$.excessReceiver = _excessReceiver;

emit ExcessReceiverSet(_excessReceiver);
}

/// @inheritdoc ICapLiquidator
function checker(address _agent, address _asset) external view returns (bool canExec, bytes memory execPayload) {
CapLiquidatorStorage storage $ = getCapLiquidatorStorage();

uint256 maxLiquidatable = ILender($.lender).maxLiquidatable(_agent, _asset);

if (maxLiquidatable > 0) {
uint256 liquidationStart = ILender($.lender).liquidationStart(_agent);
if (
block.timestamp > liquidationStart + ILender($.lender).grace()
&& block.timestamp < liquidationStart + ILender($.lender).expiry()
) {
return (true, abi.encodeCall(this.liquidate, (_agent, _asset, maxLiquidatable)));
} else {
return (false, bytes("Liquidation window not open"));
}
} else {
return (false, bytes("No liquidatable amount"));
}
}

/// @dev Check approval and increase allowance if needed
/// @param _asset Asset address
/// @param _spender Spender address
/// @param _amount Amount to approve
function _checkApproval(address _asset, address _spender, uint256 _amount) private {
uint256 allowance = IERC20(_asset).allowance(address(this), _spender);
if (allowance < _amount) {
IERC20(_asset).forceApprove(_spender, _amount);
}
}

/// @inheritdoc UUPSUpgradeable
function _authorizeUpgrade(address) internal override checkAccess(bytes4(0)) { }
}
6 changes: 6 additions & 0 deletions contracts/gelato/interfaces/ISwapRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

interface ISwapRouter {
function swapExactOut(address tokenIn, address tokenOut, uint256 amountOut) external;
}
80 changes: 80 additions & 0 deletions contracts/interfaces/ICapLiquidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

/// @title ICapLiquidator
/// @author kexley, Cap Labs
/// @notice Interface for the CapLiquidator contract
interface ICapLiquidator {
/// @notice Invalid flash loan
error InvalidFlashLoan();

/// @notice Invalid collateral
error InvalidCollateral();

/// @notice Liquidated
event Liquidated(address indexed agent, address indexed asset, address indexed collateral, uint256 excessAmount);

/// @notice Excess receiver set
event ExcessReceiverSet(address excessReceiver);

/// @dev Storage for the CapLiquidator contract
/// @param lender Lender address
/// @param balancerVault Balancer vault address
/// @param excessReceiver Excess receiver address
/// @param router Router address
/// @param delegation Delegation address
/// @param flashInProgress Flash in progress lock
struct CapLiquidatorStorage {
address lender;
address delegation;
address balancerVault;
address excessReceiver;
address router;
bool flashInProgress;
}

/// @notice Initialize the CapLiquidator contract
/// @param _accessControl Access control address
/// @param _lender Lender address
/// @param _delegation Delegation address
/// @param _balancerVault Balancer vault address
/// @param _excessReceiver Excess receiver address
/// @param _router Router address
function initialize(
address _accessControl,
address _lender,
address _delegation,
address _balancerVault,
address _excessReceiver,
address _router
) external;

/// @notice Liquidate an asset
/// @param _agent Agent address
/// @param _asset Asset address
/// @param _amount Amount of asset to liquidate
function liquidate(address _agent, address _asset, uint256 _amount) external;

/// @notice Receive a flash loan from Balancer
/// @param _assets Assets to be liquidated
/// @param _amounts Amounts of assets to be liquidated
/// @param _feeAmounts Fee amounts of assets
/// @param _userData User data
function receiveFlashLoan(
address[] memory _assets,
uint256[] memory _amounts,
uint256[] memory _feeAmounts,
bytes memory _userData
) external;

/// @notice Gelato checker
/// @param _agent Agent address
/// @param _asset Asset address
/// @return canExec Whether the checker can execute
/// @return execPayload The payload to execute
function checker(address _agent, address _asset) external view returns (bool canExec, bytes memory execPayload);

/// @notice Set the excess receiver
/// @param _excessReceiver Excess receiver address
function setExcessReceiver(address _excessReceiver) external;
}
5 changes: 5 additions & 0 deletions contracts/interfaces/IDelegation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,9 @@ interface IDelegation is IRestakerRewardReceiver {
/// @notice Get the fee recipient
/// @return recipient Fee recipient
function feeRecipient() external view returns (address recipient);

/// @notice Get the collateral address of an agent
/// @param _agent Agent address
/// @return collateral Collateral address
function collateralAddress(address _agent) external view returns (address collateral);
}
5 changes: 5 additions & 0 deletions contracts/interfaces/ISymbioticNetworkMiddleware.sol
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,9 @@ interface ISymbioticNetworkMiddleware {
/// @param _agent Agent address
/// @return vault Vault address
function vaults(address _agent) external view returns (address vault);

/// @notice Collateral address of an agent
/// @param _agent Agent address
/// @return collateral Collateral address
function collateralAddress(address _agent) external view returns (address collateral);
}
21 changes: 21 additions & 0 deletions contracts/storage/CapLiquidatorStorageUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.28;

import { ICapLiquidator } from "../interfaces/ICapLiquidator.sol";

/// @title Cap Liquidator Storage Utils
/// @author kexley, Cap Labs
/// @notice Storage utilities for cap liquidator
abstract contract CapLiquidatorStorageUtils {
/// @dev keccak256(abi.encode(uint256(keccak256("cap.storage.CapLiquidator")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant CapLiquidatorStorageLocation =
0xfcaeb3715ba096b5adee3f3404716ca7dd137705166f779f55e9930412188d00;

/// @dev Get cap liquidator storage
/// @return $ Storage pointer
function getCapLiquidatorStorage() internal pure returns (ICapLiquidator.CapLiquidatorStorage storage $) {
assembly {
$.slot := CapLiquidatorStorageLocation
}
}
}
4 changes: 2 additions & 2 deletions snapshots/Vault.gas.t.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"simple_burn": "252485",
"simple_mint": "246569"
"simple_burn": "252551",
"simple_mint": "246680"
}
6 changes: 6 additions & 0 deletions test/mocks/MockNetworkMiddleware.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
pragma solidity ^0.8.28;

import { ISymbioticNetworkMiddleware } from "../../contracts/interfaces/ISymbioticNetworkMiddleware.sol";

import { Subnetwork } from "@symbioticfi/core/src/contracts/libraries/Subnetwork.sol";
import { IVault } from "@symbioticfi/core/src/interfaces/vault/IVault.sol";

contract MockNetworkMiddleware is ISymbioticNetworkMiddleware {
SymbioticNetworkMiddlewareStorage internal _storage;
Expand Down Expand Up @@ -79,6 +81,10 @@ contract MockNetworkMiddleware is ISymbioticNetworkMiddleware {
return _storage.agentsToVault[_agent];
}

function collateralAddress(address _agent) external view returns (address collateral) {
return IVault(_storage.agentsToVault[_agent]).collateral();
}

function distributeRewards(address _agent, address _token) external {
// Mock implementation - no-op
}
Expand Down