483 lines
16 KiB
Solidity
483 lines
16 KiB
Solidity
|
|
// SPDX-License-Identifier: MIT
|
||
|
|
pragma solidity ^0.8.24;
|
||
|
|
|
||
|
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
||
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||
|
|
import "../interfaces/IKernel.sol";
|
||
|
|
import "../interfaces/IFlashLoanRouter.sol";
|
||
|
|
import "../interfaces/IVault.sol";
|
||
|
|
import "../interfaces/IConfigRegistry.sol";
|
||
|
|
import "../interfaces/IPolicyEngine.sol";
|
||
|
|
import "../interfaces/IOracleAdapter.sol";
|
||
|
|
import "../governance/GovernanceGuard.sol";
|
||
|
|
import "../core/CollateralToggleManager.sol";
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @title RecursiveLeverageKernel
|
||
|
|
* @notice Implements atomic amortizing cycles for leveraged positions
|
||
|
|
* @dev Enforces invariants: Debt↓, Collateral↑, LTV↓, HF↑
|
||
|
|
*/
|
||
|
|
contract RecursiveLeverageKernel is IKernel, IFlashLoanRouter, Ownable, AccessControl, ReentrancyGuard {
|
||
|
|
using SafeERC20 for IERC20;
|
||
|
|
|
||
|
|
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
|
||
|
|
bytes32 private constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||
|
|
|
||
|
|
// Core dependencies
|
||
|
|
IFlashLoanRouter public flashRouter;
|
||
|
|
IVault public vault;
|
||
|
|
IConfigRegistry public configRegistry;
|
||
|
|
IPolicyEngine public policyEngine;
|
||
|
|
GovernanceGuard public governanceGuard;
|
||
|
|
CollateralToggleManager public collateralManager;
|
||
|
|
IOracleAdapter public oracleAdapter;
|
||
|
|
|
||
|
|
// Aave v3 Pool for operations
|
||
|
|
interface IPoolV3 {
|
||
|
|
function supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode) external;
|
||
|
|
function withdraw(address asset, uint256 amount, address to) external returns (uint256);
|
||
|
|
function borrow(address asset, uint256 amount, uint256 interestRateMode, uint16 referralCode, address onBehalfOf) external;
|
||
|
|
function repay(address asset, uint256 amount, uint256 rateMode, address onBehalfOf) external returns (uint256);
|
||
|
|
function getUserAccountData(address user)
|
||
|
|
external
|
||
|
|
view
|
||
|
|
returns (
|
||
|
|
uint256 totalCollateralBase,
|
||
|
|
uint256 totalDebtBase,
|
||
|
|
uint256 availableBorrowsBase,
|
||
|
|
uint256 currentLiquidationThreshold,
|
||
|
|
uint256 ltv,
|
||
|
|
uint256 healthFactor
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
IPoolV3 public aavePool;
|
||
|
|
address public aaveUserAccount;
|
||
|
|
|
||
|
|
// Uniswap V3 Swap Router (simplified)
|
||
|
|
interface ISwapRouter {
|
||
|
|
struct ExactInputSingleParams {
|
||
|
|
address tokenIn;
|
||
|
|
address tokenOut;
|
||
|
|
uint24 fee;
|
||
|
|
address recipient;
|
||
|
|
uint256 deadline;
|
||
|
|
uint256 amountIn;
|
||
|
|
uint256 amountOutMinimum;
|
||
|
|
uint160 sqrtPriceLimitX96;
|
||
|
|
}
|
||
|
|
|
||
|
|
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
|
||
|
|
}
|
||
|
|
|
||
|
|
ISwapRouter public swapRouter;
|
||
|
|
|
||
|
|
// Constants
|
||
|
|
uint256 private constant HF_SCALE = 1e18;
|
||
|
|
uint256 private constant PRICE_SCALE = 1e8;
|
||
|
|
|
||
|
|
// Cycle state (for reentrancy protection)
|
||
|
|
struct CycleState {
|
||
|
|
bool inProgress;
|
||
|
|
uint256 cyclesExecuted;
|
||
|
|
uint256 totalCollateralIncrease;
|
||
|
|
uint256 totalDebtDecrease;
|
||
|
|
}
|
||
|
|
|
||
|
|
CycleState private cycleState;
|
||
|
|
|
||
|
|
// Flash loan callback state
|
||
|
|
struct FlashCallbackState {
|
||
|
|
address targetAsset;
|
||
|
|
bool inFlashLoan;
|
||
|
|
}
|
||
|
|
|
||
|
|
FlashCallbackState private flashCallbackState;
|
||
|
|
|
||
|
|
event CycleCompleted(
|
||
|
|
uint256 cyclesExecuted,
|
||
|
|
uint256 collateralIncrease,
|
||
|
|
uint256 debtDecrease,
|
||
|
|
uint256 hfImprovement
|
||
|
|
);
|
||
|
|
event SingleStepCompleted(uint256 collateralAdded, uint256 debtRepaid);
|
||
|
|
|
||
|
|
modifier onlyInCycle() {
|
||
|
|
require(cycleState.inProgress, "Not in cycle");
|
||
|
|
_;
|
||
|
|
}
|
||
|
|
|
||
|
|
modifier onlyNotInCycle() {
|
||
|
|
require(!cycleState.inProgress, "Cycle in progress");
|
||
|
|
_;
|
||
|
|
}
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
address _flashRouter,
|
||
|
|
address _vault,
|
||
|
|
address _configRegistry,
|
||
|
|
address _policyEngine,
|
||
|
|
address _governanceGuard,
|
||
|
|
address _collateralManager,
|
||
|
|
address _oracleAdapter,
|
||
|
|
address _aavePool,
|
||
|
|
address _aaveUserAccount,
|
||
|
|
address _swapRouter,
|
||
|
|
address initialOwner
|
||
|
|
) Ownable(initialOwner) {
|
||
|
|
require(_flashRouter != address(0), "Invalid flash router");
|
||
|
|
require(_vault != address(0), "Invalid vault");
|
||
|
|
require(_configRegistry != address(0), "Invalid config registry");
|
||
|
|
require(_policyEngine != address(0), "Invalid policy engine");
|
||
|
|
require(_governanceGuard != address(0), "Invalid governance guard");
|
||
|
|
require(_collateralManager != address(0), "Invalid collateral manager");
|
||
|
|
require(_oracleAdapter != address(0), "Invalid oracle adapter");
|
||
|
|
require(_aavePool != address(0), "Invalid Aave pool");
|
||
|
|
require(_aaveUserAccount != address(0), "Invalid Aave account");
|
||
|
|
|
||
|
|
flashRouter = IFlashLoanRouter(_flashRouter);
|
||
|
|
vault = IVault(_vault);
|
||
|
|
configRegistry = IConfigRegistry(_configRegistry);
|
||
|
|
policyEngine = IPolicyEngine(_policyEngine);
|
||
|
|
governanceGuard = GovernanceGuard(_governanceGuard);
|
||
|
|
collateralManager = CollateralToggleManager(_collateralManager);
|
||
|
|
oracleAdapter = IOracleAdapter(_oracleAdapter);
|
||
|
|
aavePool = IPoolV3(_aavePool);
|
||
|
|
aaveUserAccount = _aaveUserAccount;
|
||
|
|
swapRouter = ISwapRouter(_swapRouter);
|
||
|
|
|
||
|
|
_grantRole(DEFAULT_ADMIN_ROLE, initialOwner);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Execute atomic amortizing cycle
|
||
|
|
* @dev Main entry point for amortization strategy
|
||
|
|
*/
|
||
|
|
function executeAmortizingCycle(
|
||
|
|
AmortizationParams memory params
|
||
|
|
) external override onlyRole(OPERATOR_ROLE) nonReentrant onlyNotInCycle returns (bool success, uint256 cyclesExecuted) {
|
||
|
|
// Enforce policy checks
|
||
|
|
bytes memory actionData = abi.encode(
|
||
|
|
address(vault),
|
||
|
|
vault.getHealthFactor(),
|
||
|
|
params.targetAsset,
|
||
|
|
params.maxLoops
|
||
|
|
);
|
||
|
|
governanceGuard.enforceInvariants(keccak256("AMORTIZATION"), actionData);
|
||
|
|
|
||
|
|
// Check max loops from config
|
||
|
|
uint256 maxLoops = configRegistry.getMaxLoops();
|
||
|
|
require(params.maxLoops <= maxLoops, "Exceeds max loops");
|
||
|
|
|
||
|
|
// Take position snapshot
|
||
|
|
(uint256 collateralBefore, uint256 debtBefore, uint256 healthFactorBefore) = vault.snapshotPosition();
|
||
|
|
|
||
|
|
// Initialize cycle state
|
||
|
|
cycleState = CycleState({
|
||
|
|
inProgress: true,
|
||
|
|
cyclesExecuted: 0,
|
||
|
|
totalCollateralIncrease: 0,
|
||
|
|
totalDebtDecrease: 0
|
||
|
|
});
|
||
|
|
|
||
|
|
// Execute cycles up to maxLoops
|
||
|
|
uint256 actualLoops = params.maxLoops;
|
||
|
|
for (uint256 i = 0; i < params.maxLoops; i++) {
|
||
|
|
// Calculate optimal flash loan amount (simplified)
|
||
|
|
uint256 flashAmount = _calculateOptimalFlashAmount();
|
||
|
|
|
||
|
|
if (flashAmount == 0) {
|
||
|
|
actualLoops = i;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Execute single step
|
||
|
|
(uint256 collateralAdded, uint256 debtRepaid) = _executeSingleStepInternal(
|
||
|
|
flashAmount,
|
||
|
|
params.targetAsset
|
||
|
|
);
|
||
|
|
|
||
|
|
if (collateralAdded == 0 && debtRepaid == 0) {
|
||
|
|
actualLoops = i;
|
||
|
|
break; // No improvement possible
|
||
|
|
}
|
||
|
|
|
||
|
|
cycleState.cyclesExecuted++;
|
||
|
|
cycleState.totalCollateralIncrease += collateralAdded;
|
||
|
|
cycleState.totalDebtDecrease += debtRepaid;
|
||
|
|
|
||
|
|
// Check if minimum HF improvement achieved
|
||
|
|
uint256 currentHF = vault.getHealthFactor();
|
||
|
|
if (currentHF >= healthFactorBefore + params.minHFImprovement) {
|
||
|
|
break; // Early exit if target achieved
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Verify invariants
|
||
|
|
(bool invariantSuccess, string memory reason) = verifyInvariants(
|
||
|
|
collateralBefore,
|
||
|
|
debtBefore,
|
||
|
|
healthFactorBefore
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!invariantSuccess) {
|
||
|
|
emit InvariantFail(reason);
|
||
|
|
revert(reason);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate improvements
|
||
|
|
uint256 collateralAfter = vault.getTotalCollateralValue();
|
||
|
|
uint256 debtAfter = vault.getTotalDebtValue();
|
||
|
|
uint256 healthFactorAfter = vault.getHealthFactor();
|
||
|
|
|
||
|
|
uint256 hfImprovement = healthFactorAfter > healthFactorBefore
|
||
|
|
? healthFactorAfter - healthFactorBefore
|
||
|
|
: 0;
|
||
|
|
|
||
|
|
// Emit events
|
||
|
|
emit AmortizationExecuted(
|
||
|
|
cycleState.cyclesExecuted,
|
||
|
|
collateralAfter > collateralBefore ? collateralAfter - collateralBefore : 0,
|
||
|
|
debtBefore > debtAfter ? debtBefore - debtAfter : 0,
|
||
|
|
hfImprovement
|
||
|
|
);
|
||
|
|
|
||
|
|
emit CycleCompleted(
|
||
|
|
cycleState.cyclesExecuted,
|
||
|
|
cycleState.totalCollateralIncrease,
|
||
|
|
cycleState.totalDebtDecrease,
|
||
|
|
hfImprovement
|
||
|
|
);
|
||
|
|
|
||
|
|
success = true;
|
||
|
|
cyclesExecuted = cycleState.cyclesExecuted;
|
||
|
|
|
||
|
|
// Clear state
|
||
|
|
cycleState.inProgress = false;
|
||
|
|
delete cycleState;
|
||
|
|
|
||
|
|
return (success, cyclesExecuted);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Execute a single amortization step
|
||
|
|
*/
|
||
|
|
function executeSingleStep(
|
||
|
|
IFlashLoanRouter.FlashLoanParams memory flashLoanParams,
|
||
|
|
address targetAsset
|
||
|
|
) external override onlyRole(OPERATOR_ROLE) nonReentrant onlyNotInCycle returns (uint256 collateralAdded, uint256 debtRepaid) {
|
||
|
|
// Enforce policy checks
|
||
|
|
bytes memory actionData = abi.encode(
|
||
|
|
address(vault),
|
||
|
|
flashLoanParams.asset,
|
||
|
|
flashLoanParams.amount
|
||
|
|
);
|
||
|
|
governanceGuard.enforceInvariants(keccak256("FLASH_LOAN"), actionData);
|
||
|
|
|
||
|
|
cycleState.inProgress = true;
|
||
|
|
(collateralAdded, debtRepaid) = _executeSingleStepInternal(flashLoanParams.amount, targetAsset);
|
||
|
|
cycleState.inProgress = false;
|
||
|
|
delete cycleState;
|
||
|
|
|
||
|
|
emit SingleStepCompleted(collateralAdded, debtRepaid);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Internal single step execution
|
||
|
|
*/
|
||
|
|
function _executeSingleStepInternal(
|
||
|
|
uint256 flashAmount,
|
||
|
|
address targetAsset
|
||
|
|
) internal returns (uint256 collateralAdded, uint256 debtRepaid) {
|
||
|
|
// Set up flash callback state
|
||
|
|
flashCallbackState = FlashCallbackState({
|
||
|
|
targetAsset: targetAsset,
|
||
|
|
inFlashLoan: true
|
||
|
|
});
|
||
|
|
|
||
|
|
// Determine best flash loan provider (simplified - use Aave by default)
|
||
|
|
IFlashLoanRouter.FlashLoanParams memory params = IFlashLoanRouter.FlashLoanParams({
|
||
|
|
asset: targetAsset, // Would determine optimal asset in production
|
||
|
|
amount: flashAmount,
|
||
|
|
provider: IFlashLoanRouter.FlashLoanProvider.AAVE
|
||
|
|
});
|
||
|
|
|
||
|
|
bytes memory callbackData = abi.encode(targetAsset);
|
||
|
|
|
||
|
|
// Execute flash loan (this will call onFlashLoan callback)
|
||
|
|
flashRouter.flashLoan(params, callbackData);
|
||
|
|
|
||
|
|
// Clear flash callback state
|
||
|
|
delete flashCallbackState;
|
||
|
|
|
||
|
|
// Calculate improvements (would track during callback)
|
||
|
|
// For now, return placeholder
|
||
|
|
collateralAdded = flashAmount / 2; // Simplified: 50% to collateral
|
||
|
|
debtRepaid = flashAmount / 2; // 50% to debt repayment
|
||
|
|
|
||
|
|
// Record in vault
|
||
|
|
vault.recordCollateralAdded(targetAsset, collateralAdded);
|
||
|
|
vault.recordDebtRepaid(targetAsset, debtRepaid);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Flash loan callback
|
||
|
|
*/
|
||
|
|
function onFlashLoan(
|
||
|
|
address asset,
|
||
|
|
uint256 amount,
|
||
|
|
uint256 fee,
|
||
|
|
bytes calldata callbackData
|
||
|
|
) external override returns (bytes32) {
|
||
|
|
require(msg.sender == address(flashRouter), "Invalid caller");
|
||
|
|
require(flashCallbackState.inFlashLoan, "Not in flash loan");
|
||
|
|
|
||
|
|
address targetAsset = flashCallbackState.targetAsset;
|
||
|
|
if (targetAsset == address(0)) {
|
||
|
|
(targetAsset) = abi.decode(callbackData, (address));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 1. Harvest yield (simplified - would claim rewards from Aave/other protocols)
|
||
|
|
uint256 yieldAmount = _harvestYield(targetAsset);
|
||
|
|
|
||
|
|
// 2. Swap yield to target asset (if needed)
|
||
|
|
uint256 totalAmount = amount + yieldAmount;
|
||
|
|
if (asset != targetAsset) {
|
||
|
|
totalAmount = _swapAsset(asset, targetAsset, totalAmount);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Split: repay debt + add collateral
|
||
|
|
uint256 repayAmount = totalAmount / 2;
|
||
|
|
uint256 collateralAmount = totalAmount - repayAmount;
|
||
|
|
|
||
|
|
// Repay debt
|
||
|
|
_repayDebt(targetAsset, repayAmount);
|
||
|
|
|
||
|
|
// Add collateral
|
||
|
|
_addCollateral(targetAsset, collateralAmount);
|
||
|
|
|
||
|
|
// Repay flash loan
|
||
|
|
uint256 repaymentAmount = amount + fee;
|
||
|
|
IERC20(asset).safeApprove(address(flashRouter), repaymentAmount);
|
||
|
|
|
||
|
|
return CALLBACK_SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Harvest yield from protocols
|
||
|
|
*/
|
||
|
|
function _harvestYield(address asset) internal returns (uint256 yieldAmount) {
|
||
|
|
// Simplified: would claim rewards from Aave, compound, etc.
|
||
|
|
// For now, return 0
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Swap asset using Uniswap V3
|
||
|
|
*/
|
||
|
|
function _swapAsset(
|
||
|
|
address tokenIn,
|
||
|
|
address tokenOut,
|
||
|
|
uint256 amountIn
|
||
|
|
) internal returns (uint256 amountOut) {
|
||
|
|
// Approve router
|
||
|
|
IERC20(tokenIn).safeApprove(address(swapRouter), amountIn);
|
||
|
|
|
||
|
|
// Execute swap (simplified - would use proper fee tier and price limits)
|
||
|
|
ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
|
||
|
|
tokenIn: tokenIn,
|
||
|
|
tokenOut: tokenOut,
|
||
|
|
fee: 3000, // 0.3% fee tier
|
||
|
|
recipient: address(this),
|
||
|
|
deadline: block.timestamp + 300,
|
||
|
|
amountIn: amountIn,
|
||
|
|
amountOutMinimum: 0, // Would calculate in production
|
||
|
|
sqrtPriceLimitX96: 0
|
||
|
|
});
|
||
|
|
|
||
|
|
return swapRouter.exactInputSingle(params);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Repay debt
|
||
|
|
*/
|
||
|
|
function _repayDebt(address asset, uint256 amount) internal {
|
||
|
|
// Approve Aave
|
||
|
|
IERC20(asset).safeApprove(address(aavePool), amount);
|
||
|
|
|
||
|
|
// Repay (variable rate mode = 2)
|
||
|
|
aavePool.repay(asset, amount, 2, aaveUserAccount);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Add collateral
|
||
|
|
*/
|
||
|
|
function _addCollateral(address asset, uint256 amount) internal {
|
||
|
|
// Approve Aave
|
||
|
|
IERC20(asset).safeApprove(address(aavePool), amount);
|
||
|
|
|
||
|
|
// Supply as collateral
|
||
|
|
aavePool.supply(asset, amount, aaveUserAccount, 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Verify invariants
|
||
|
|
*/
|
||
|
|
function verifyInvariants(
|
||
|
|
uint256 collateralBefore,
|
||
|
|
uint256 debtBefore,
|
||
|
|
uint256 healthFactorBefore
|
||
|
|
) public view override returns (bool success, string memory reason) {
|
||
|
|
return vault.verifyPositionImproved(collateralBefore, debtBefore, healthFactorBefore)
|
||
|
|
? (true, "")
|
||
|
|
: (false, "Position did not improve");
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Calculate optimal flash loan amount
|
||
|
|
*/
|
||
|
|
function _calculateOptimalFlashAmount() internal view returns (uint256) {
|
||
|
|
// Simplified: use a percentage of available borrow capacity
|
||
|
|
try aavePool.getUserAccountData(aaveUserAccount) returns (
|
||
|
|
uint256,
|
||
|
|
uint256,
|
||
|
|
uint256 availableBorrowsBase,
|
||
|
|
uint256,
|
||
|
|
uint256,
|
||
|
|
uint256
|
||
|
|
) {
|
||
|
|
// Use 50% of available borrows
|
||
|
|
return availableBorrowsBase / 2;
|
||
|
|
} catch {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @notice Update dependencies
|
||
|
|
*/
|
||
|
|
function setFlashRouter(address newRouter) external onlyOwner {
|
||
|
|
require(newRouter != address(0), "Invalid router");
|
||
|
|
flashRouter = IFlashLoanRouter(newRouter);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setConfigRegistry(address newRegistry) external onlyOwner {
|
||
|
|
require(newRegistry != address(0), "Invalid registry");
|
||
|
|
configRegistry = IConfigRegistry(newRegistry);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setPolicyEngine(address newEngine) external onlyOwner {
|
||
|
|
require(newEngine != address(0), "Invalid engine");
|
||
|
|
policyEngine = IPolicyEngine(newEngine);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setGovernanceGuard(address newGuard) external onlyOwner {
|
||
|
|
require(newGuard != address(0), "Invalid guard");
|
||
|
|
governanceGuard = GovernanceGuard(newGuard);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|