Initial commit
This commit is contained in:
176
contracts/core/CollateralToggleManager.sol
Normal file
176
contracts/core/CollateralToggleManager.sol
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* @title CollateralToggleManager
|
||||
* @notice Manages Aave v3 collateral enable/disable and sub-vault operations
|
||||
* @dev Enables efficient batch operations for collateral management
|
||||
*/
|
||||
contract CollateralToggleManager is Ownable, AccessControl, ReentrancyGuard {
|
||||
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
|
||||
bytes32 public constant KERNEL_ROLE = keccak256("KERNEL_ROLE");
|
||||
|
||||
// Aave v3 Pool interface
|
||||
interface IPoolV3 {
|
||||
function setUserUseReserveAsCollateral(
|
||||
address asset,
|
||||
bool useAsCollateral
|
||||
) external;
|
||||
|
||||
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;
|
||||
|
||||
// Collateral status tracking
|
||||
mapping(address => bool) public collateralEnabled;
|
||||
address[] public managedAssets;
|
||||
|
||||
event CollateralToggled(address indexed asset, bool enabled);
|
||||
event BatchCollateralToggled(address[] assets, bool[] enabled);
|
||||
event AavePoolUpdated(address oldPool, address newPool);
|
||||
event AaveUserAccountUpdated(address oldAccount, address newAccount);
|
||||
|
||||
constructor(
|
||||
address _aavePool,
|
||||
address _aaveUserAccount,
|
||||
address initialOwner
|
||||
) Ownable(initialOwner) {
|
||||
require(_aavePool != address(0), "Invalid Aave pool");
|
||||
require(_aaveUserAccount != address(0), "Invalid Aave account");
|
||||
|
||||
aavePool = IPoolV3(_aavePool);
|
||||
aaveUserAccount = _aaveUserAccount;
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, initialOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable or disable collateral for an asset
|
||||
*/
|
||||
function toggleCollateral(
|
||||
address asset,
|
||||
bool useAsCollateral
|
||||
) external onlyRole(KERNEL_ROLE) nonReentrant {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
|
||||
// Update Aave
|
||||
aavePool.setUserUseReserveAsCollateral(asset, useAsCollateral);
|
||||
|
||||
// Track status
|
||||
if (!collateralEnabled[asset] && managedAssets.length > 0) {
|
||||
bool alreadyTracked = false;
|
||||
for (uint256 i = 0; i < managedAssets.length; i++) {
|
||||
if (managedAssets[i] == asset) {
|
||||
alreadyTracked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!alreadyTracked) {
|
||||
managedAssets.push(asset);
|
||||
}
|
||||
}
|
||||
|
||||
collateralEnabled[asset] = useAsCollateral;
|
||||
emit CollateralToggled(asset, useAsCollateral);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Batch toggle collateral for multiple assets
|
||||
*/
|
||||
function batchToggleCollateral(
|
||||
address[] calldata assets,
|
||||
bool[] calldata useAsCollateral
|
||||
) external onlyRole(KERNEL_ROLE) nonReentrant {
|
||||
require(assets.length == useAsCollateral.length, "Array length mismatch");
|
||||
require(assets.length > 0 && assets.length <= 20, "Invalid array length"); // Reasonable limit
|
||||
|
||||
for (uint256 i = 0; i < assets.length; i++) {
|
||||
toggleCollateral(assets[i], useAsCollateral[i]);
|
||||
}
|
||||
|
||||
emit BatchCollateralToggled(assets, useAsCollateral);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get collateral status for an asset
|
||||
*/
|
||||
function isCollateralEnabled(address asset) external view returns (bool) {
|
||||
return collateralEnabled[asset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get all managed assets
|
||||
*/
|
||||
function getManagedAssets() external view returns (address[] memory) {
|
||||
return managedAssets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get Aave position health factor
|
||||
*/
|
||||
function getHealthFactor() external view returns (uint256) {
|
||||
try aavePool.getUserAccountData(aaveUserAccount) returns (
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256 healthFactor
|
||||
) {
|
||||
return healthFactor;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update Aave pool
|
||||
*/
|
||||
function setAavePool(address newPool) external onlyOwner {
|
||||
require(newPool != address(0), "Invalid pool");
|
||||
address oldPool = address(aavePool);
|
||||
aavePool = IPoolV3(newPool);
|
||||
emit AavePoolUpdated(oldPool, newPool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update Aave user account
|
||||
*/
|
||||
function setAaveUserAccount(address newAccount) external onlyOwner {
|
||||
require(newAccount != address(0), "Invalid account");
|
||||
address oldAccount = aaveUserAccount;
|
||||
aaveUserAccount = newAccount;
|
||||
emit AaveUserAccountUpdated(oldAccount, newAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Grant kernel role
|
||||
*/
|
||||
function grantKernel(address kernel) external onlyOwner {
|
||||
_grantRole(KERNEL_ROLE, kernel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Revoke kernel role
|
||||
*/
|
||||
function revokeKernel(address kernel) external onlyOwner {
|
||||
_revokeRole(KERNEL_ROLE, kernel);
|
||||
}
|
||||
}
|
||||
|
||||
318
contracts/core/DBISInstitutionalVault.sol
Normal file
318
contracts/core/DBISInstitutionalVault.sol
Normal file
@@ -0,0 +1,318 @@
|
||||
// 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 "../interfaces/IVault.sol";
|
||||
import "../interfaces/IOracleAdapter.sol";
|
||||
|
||||
/**
|
||||
* @title DBISInstitutionalVault
|
||||
* @notice Institutional vault representing a leveraged DeFi position
|
||||
* @dev Tracks collateral and debt across multiple assets, enforces invariants
|
||||
*/
|
||||
contract DBISInstitutionalVault is IVault, Ownable, AccessControl, ReentrancyGuard {
|
||||
bytes32 public constant OPERATOR_ROLE = keccak256("OPERATOR_ROLE");
|
||||
bytes32 public constant KERNEL_ROLE = keccak256("KERNEL_ROLE");
|
||||
|
||||
IOracleAdapter public oracleAdapter;
|
||||
|
||||
// Aave v3 Pool interface (simplified)
|
||||
interface IPoolV3 {
|
||||
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; // Address of the Aave position
|
||||
|
||||
// Internal position tracking (for non-Aave assets)
|
||||
struct AssetPosition {
|
||||
uint256 collateral; // Amount deposited as collateral
|
||||
uint256 debt; // Amount borrowed
|
||||
}
|
||||
|
||||
mapping(address => AssetPosition) private assetPositions;
|
||||
address[] private trackedAssets;
|
||||
|
||||
// Constants
|
||||
uint256 private constant HF_SCALE = 1e18;
|
||||
uint256 private constant PRICE_SCALE = 1e8;
|
||||
|
||||
constructor(
|
||||
address _oracleAdapter,
|
||||
address _aavePool,
|
||||
address _aaveUserAccount,
|
||||
address initialOwner
|
||||
) Ownable(initialOwner) {
|
||||
require(_oracleAdapter != address(0), "Invalid oracle");
|
||||
require(_aavePool != address(0), "Invalid Aave pool");
|
||||
require(_aaveUserAccount != address(0), "Invalid Aave account");
|
||||
|
||||
oracleAdapter = IOracleAdapter(_oracleAdapter);
|
||||
aavePool = IPoolV3(_aavePool);
|
||||
aaveUserAccount = _aaveUserAccount;
|
||||
|
||||
_grantRole(DEFAULT_ADMIN_ROLE, initialOwner);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get total collateral value in USD (scaled by 1e8)
|
||||
*/
|
||||
function getTotalCollateralValue() public view override returns (uint256) {
|
||||
uint256 total = 0;
|
||||
|
||||
// Get Aave collateral
|
||||
try aavePool.getUserAccountData(aaveUserAccount) returns (
|
||||
uint256 totalCollateralBase,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256
|
||||
) {
|
||||
// Aave returns in base currency (USD, scaled by 1e8)
|
||||
total += totalCollateralBase;
|
||||
} catch {}
|
||||
|
||||
// Add non-Aave collateral
|
||||
for (uint256 i = 0; i < trackedAssets.length; i++) {
|
||||
address asset = trackedAssets[i];
|
||||
AssetPosition storage pos = assetPositions[asset];
|
||||
if (pos.collateral > 0) {
|
||||
try oracleAdapter.convertAmount(asset, pos.collateral, address(0)) returns (uint256 value) {
|
||||
total += value;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get total debt value in USD (scaled by 1e8)
|
||||
*/
|
||||
function getTotalDebtValue() public view override returns (uint256) {
|
||||
uint256 total = 0;
|
||||
|
||||
// Get Aave debt
|
||||
try aavePool.getUserAccountData(aaveUserAccount) returns (
|
||||
uint256,
|
||||
uint256 totalDebtBase,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256
|
||||
) {
|
||||
// Aave returns in base currency (USD, scaled by 1e8)
|
||||
total += totalDebtBase;
|
||||
} catch {}
|
||||
|
||||
// Add non-Aave debt
|
||||
for (uint256 i = 0; i < trackedAssets.length; i++) {
|
||||
address asset = trackedAssets[i];
|
||||
AssetPosition storage pos = assetPositions[asset];
|
||||
if (pos.debt > 0) {
|
||||
try oracleAdapter.convertAmount(asset, pos.debt, address(0)) returns (uint256 value) {
|
||||
total += value;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current health factor (scaled by 1e18)
|
||||
*/
|
||||
function getHealthFactor() public view override returns (uint256) {
|
||||
// Try to get from Aave first (most accurate)
|
||||
try aavePool.getUserAccountData(aaveUserAccount) returns (
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256,
|
||||
uint256 healthFactor
|
||||
) {
|
||||
return healthFactor;
|
||||
} catch {}
|
||||
|
||||
// Fallback: calculate manually
|
||||
uint256 collateralValue = getTotalCollateralValue();
|
||||
uint256 debtValue = getTotalDebtValue();
|
||||
|
||||
if (debtValue == 0) {
|
||||
return type(uint256).max; // Infinite health factor if no debt
|
||||
}
|
||||
|
||||
// Health Factor = (Collateral * Liquidation Threshold) / Debt
|
||||
// Simplified: use 80% LTV as threshold
|
||||
return (collateralValue * 80e18 / 100) / debtValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current LTV (Loan-to-Value ratio, scaled by 1e18)
|
||||
*/
|
||||
function getLTV() public view override returns (uint256) {
|
||||
uint256 collateralValue = getTotalCollateralValue();
|
||||
if (collateralValue == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 debtValue = getTotalDebtValue();
|
||||
return (debtValue * HF_SCALE) / collateralValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record addition of collateral
|
||||
*/
|
||||
function recordCollateralAdded(address asset, uint256 amount) external override onlyRole(KERNEL_ROLE) {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
require(amount > 0, "Invalid amount");
|
||||
|
||||
if (assetPositions[asset].collateral == 0 && assetPositions[asset].debt == 0) {
|
||||
trackedAssets.push(asset);
|
||||
}
|
||||
|
||||
assetPositions[asset].collateral += amount;
|
||||
emit CollateralAdded(asset, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record repayment of debt
|
||||
*/
|
||||
function recordDebtRepaid(address asset, uint256 amount) external override onlyRole(KERNEL_ROLE) {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
require(amount > 0, "Invalid amount");
|
||||
require(assetPositions[asset].debt >= amount, "Debt insufficient");
|
||||
|
||||
assetPositions[asset].debt -= amount;
|
||||
emit DebtRepaid(asset, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Take a position snapshot for invariant checking
|
||||
*/
|
||||
function snapshotPosition()
|
||||
external
|
||||
override
|
||||
onlyRole(KERNEL_ROLE)
|
||||
returns (
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 healthFactorBefore
|
||||
)
|
||||
{
|
||||
collateralBefore = getTotalCollateralValue();
|
||||
debtBefore = getTotalDebtValue();
|
||||
healthFactorBefore = getHealthFactor();
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify position improved (invariant check)
|
||||
* @dev Enforces: Debt↓ OR Collateral↑ OR HF↑
|
||||
*/
|
||||
function verifyPositionImproved(
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 healthFactorBefore
|
||||
) external view override returns (bool success) {
|
||||
uint256 collateralAfter = getTotalCollateralValue();
|
||||
uint256 debtAfter = getTotalDebtValue();
|
||||
uint256 healthFactorAfter = getHealthFactor();
|
||||
|
||||
// Emit snapshot event (best effort)
|
||||
// Note: Events can't be emitted from view functions in Solidity
|
||||
// This would be done in the calling contract
|
||||
|
||||
// Check invariants:
|
||||
// 1. Debt decreased OR
|
||||
// 2. Collateral increased OR
|
||||
// 3. Health factor improved
|
||||
bool debtDecreased = debtAfter < debtBefore;
|
||||
bool collateralIncreased = collateralAfter > collateralBefore;
|
||||
bool hfImproved = healthFactorAfter > healthFactorBefore;
|
||||
|
||||
// All three must improve for strict amortization
|
||||
return debtDecreased && collateralIncreased && hfImproved;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Grant operator role
|
||||
*/
|
||||
function grantOperator(address operator) external onlyOwner {
|
||||
_grantRole(OPERATOR_ROLE, operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Grant kernel role
|
||||
*/
|
||||
function grantKernel(address kernel) external onlyOwner {
|
||||
_grantRole(KERNEL_ROLE, kernel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Revoke operator role
|
||||
*/
|
||||
function revokeOperator(address operator) external onlyOwner {
|
||||
_revokeRole(OPERATOR_ROLE, operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Revoke kernel role
|
||||
*/
|
||||
function revokeKernel(address kernel) external onlyOwner {
|
||||
_revokeRole(KERNEL_ROLE, kernel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update oracle adapter
|
||||
*/
|
||||
function setOracleAdapter(address newOracle) external onlyOwner {
|
||||
require(newOracle != address(0), "Invalid oracle");
|
||||
oracleAdapter = IOracleAdapter(newOracle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update Aave pool
|
||||
*/
|
||||
function setAavePool(address newPool) external onlyOwner {
|
||||
require(newPool != address(0), "Invalid pool");
|
||||
aavePool = IPoolV3(newPool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update Aave user account
|
||||
*/
|
||||
function setAaveUserAccount(address newAccount) external onlyOwner {
|
||||
require(newAccount != address(0), "Invalid account");
|
||||
aaveUserAccount = newAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get asset position
|
||||
*/
|
||||
function getAssetPosition(address asset) external view returns (uint256 collateral, uint256 debt) {
|
||||
AssetPosition storage pos = assetPositions[asset];
|
||||
return (pos.collateral, pos.debt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get all tracked assets
|
||||
*/
|
||||
function getTrackedAssets() external view returns (address[] memory) {
|
||||
return trackedAssets;
|
||||
}
|
||||
}
|
||||
|
||||
354
contracts/core/FlashLoanRouter.sol
Normal file
354
contracts/core/FlashLoanRouter.sol
Normal file
@@ -0,0 +1,354 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||||
import "../interfaces/IFlashLoanRouter.sol";
|
||||
|
||||
/**
|
||||
* @title FlashLoanRouter
|
||||
* @notice Multi-provider flash loan aggregator
|
||||
* @dev Routes flash loans to Aave, Balancer, Uniswap V3, or DAI flash mint
|
||||
*/
|
||||
contract FlashLoanRouter is IFlashLoanRouter, Ownable, ReentrancyGuard {
|
||||
using SafeERC20 for IERC20;
|
||||
|
||||
// Callback interface
|
||||
interface IFlashLoanReceiver {
|
||||
function onFlashLoan(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 fee,
|
||||
bytes calldata data
|
||||
) external returns (bytes32);
|
||||
}
|
||||
|
||||
// Aave v3 Pool
|
||||
interface IPoolV3 {
|
||||
function flashLoanSimple(
|
||||
address receiverAddress,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
bytes calldata params,
|
||||
uint16 referralCode
|
||||
) external;
|
||||
}
|
||||
|
||||
// Balancer Vault
|
||||
interface IBalancerVault {
|
||||
function flashLoan(
|
||||
address recipient,
|
||||
address[] memory tokens,
|
||||
uint256[] memory amounts,
|
||||
bytes memory userData
|
||||
) external;
|
||||
}
|
||||
|
||||
// Uniswap V3 Pool
|
||||
interface IUniswapV3Pool {
|
||||
function flash(
|
||||
address recipient,
|
||||
uint256 amount0,
|
||||
uint256 amount1,
|
||||
bytes calldata data
|
||||
) external;
|
||||
}
|
||||
|
||||
// DAI Flash Mint
|
||||
interface IDaiFlashMint {
|
||||
function flashMint(
|
||||
address receiver,
|
||||
uint256 amount,
|
||||
bytes calldata data
|
||||
) external;
|
||||
}
|
||||
|
||||
// Provider addresses
|
||||
address public aavePool;
|
||||
address public balancerVault;
|
||||
address public daiFlashMint;
|
||||
|
||||
// Constants
|
||||
bytes32 public constant CALLBACK_SUCCESS = keccak256("ERC3156FlashBorrower.onFlashLoan");
|
||||
uint16 public constant AAVE_REFERRAL_CODE = 0;
|
||||
|
||||
// Flash loan state
|
||||
struct FlashLoanState {
|
||||
address caller;
|
||||
FlashLoanProvider provider;
|
||||
bytes callbackData;
|
||||
bool inProgress;
|
||||
}
|
||||
|
||||
FlashLoanState private flashLoanState;
|
||||
|
||||
event ProviderAddressUpdated(FlashLoanProvider provider, address oldAddress, address newAddress);
|
||||
|
||||
modifier onlyInFlashLoan() {
|
||||
require(flashLoanState.inProgress, "Not in flash loan");
|
||||
_;
|
||||
}
|
||||
|
||||
modifier onlyNotInFlashLoan() {
|
||||
require(!flashLoanState.inProgress, "Flash loan in progress");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(
|
||||
address _aavePool,
|
||||
address _balancerVault,
|
||||
address _daiFlashMint,
|
||||
address initialOwner
|
||||
) Ownable(initialOwner) {
|
||||
aavePool = _aavePool;
|
||||
balancerVault = _balancerVault;
|
||||
daiFlashMint = _daiFlashMint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Execute a flash loan
|
||||
*/
|
||||
function flashLoan(
|
||||
FlashLoanParams memory params,
|
||||
bytes memory callbackData
|
||||
) external override nonReentrant onlyNotInFlashLoan {
|
||||
require(params.asset != address(0), "Invalid asset");
|
||||
require(params.amount > 0, "Invalid amount");
|
||||
|
||||
// Determine provider if needed (for liquidity-weighted selection)
|
||||
FlashLoanProvider provider = params.provider;
|
||||
|
||||
// Set flash loan state
|
||||
flashLoanState = FlashLoanState({
|
||||
caller: msg.sender,
|
||||
provider: provider,
|
||||
callbackData: callbackData,
|
||||
inProgress: true
|
||||
});
|
||||
|
||||
// Execute flash loan based on provider
|
||||
if (provider == FlashLoanProvider.AAVE) {
|
||||
_flashLoanAave(params.asset, params.amount);
|
||||
} else if (provider == FlashLoanProvider.BALANCER) {
|
||||
_flashLoanBalancer(params.asset, params.amount);
|
||||
} else if (provider == FlashLoanProvider.UNISWAP) {
|
||||
_flashLoanUniswap(params.asset, params.amount);
|
||||
} else if (provider == FlashLoanProvider.DAI_FLASH) {
|
||||
_flashLoanDai(params.amount);
|
||||
} else {
|
||||
revert("Invalid provider");
|
||||
}
|
||||
|
||||
// Clear state
|
||||
flashLoanState.inProgress = false;
|
||||
delete flashLoanState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Execute multi-asset flash loan
|
||||
*/
|
||||
function flashLoanBatch(
|
||||
FlashLoanParams[] memory params,
|
||||
bytes memory callbackData
|
||||
) external override nonReentrant onlyNotInFlashLoan {
|
||||
require(params.length > 0, "Empty params");
|
||||
require(params.length <= 10, "Too many assets"); // Reasonable limit
|
||||
|
||||
// For simplicity, execute sequentially
|
||||
// In production, could optimize for parallel execution
|
||||
for (uint256 i = 0; i < params.length; i++) {
|
||||
flashLoan(params[i], callbackData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Aave v3 flash loan
|
||||
*/
|
||||
function _flashLoanAave(address asset, uint256 amount) internal {
|
||||
require(aavePool != address(0), "Aave pool not set");
|
||||
|
||||
emit FlashLoanInitiated(asset, amount, FlashLoanProvider.AAVE);
|
||||
|
||||
bytes memory params = abi.encode(flashLoanState.caller, flashLoanState.callbackData);
|
||||
IPoolV3(aavePool).flashLoanSimple(
|
||||
address(this),
|
||||
asset,
|
||||
amount,
|
||||
params,
|
||||
AAVE_REFERRAL_CODE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Balancer flash loan
|
||||
*/
|
||||
function _flashLoanBalancer(address asset, uint256 amount) internal {
|
||||
require(balancerVault != address(0), "Balancer vault not set");
|
||||
|
||||
emit FlashLoanInitiated(asset, amount, FlashLoanProvider.BALANCER);
|
||||
|
||||
address[] memory tokens = new address[](1);
|
||||
tokens[0] = asset;
|
||||
uint256[] memory amounts = new uint256[](1);
|
||||
amounts[0] = amount;
|
||||
|
||||
bytes memory userData = abi.encode(flashLoanState.caller, flashLoanState.callbackData);
|
||||
IBalancerVault(balancerVault).flashLoan(address(this), tokens, amounts, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Uniswap V3 flash loan
|
||||
*/
|
||||
function _flashLoanUniswap(address asset, uint256 amount) internal {
|
||||
// Uniswap V3 requires pool address - simplified here
|
||||
// In production, would need to determine pool from asset pair
|
||||
revert("Uniswap V3 flash loan not fully implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice DAI flash mint
|
||||
*/
|
||||
function _flashLoanDai(uint256 amount) internal {
|
||||
require(daiFlashMint != address(0), "DAI flash mint not set");
|
||||
|
||||
emit FlashLoanInitiated(address(0), amount, FlashLoanProvider.DAI_FLASH); // DAI address
|
||||
|
||||
bytes memory data = abi.encode(flashLoanState.caller, flashLoanState.callbackData);
|
||||
IDaiFlashMint(daiFlashMint).flashMint(address(this), amount, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Aave flash loan callback
|
||||
*/
|
||||
function executeOperation(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 premium,
|
||||
bytes calldata params
|
||||
) external returns (bool) {
|
||||
require(msg.sender == aavePool, "Invalid caller");
|
||||
require(flashLoanState.inProgress, "Not in flash loan");
|
||||
|
||||
(address receiver, bytes memory callbackData) = abi.decode(params, (address, bytes));
|
||||
|
||||
// Calculate total repayment
|
||||
uint256 totalRepayment = amount + premium;
|
||||
|
||||
// Call receiver callback
|
||||
bytes32 result = IFlashLoanReceiver(receiver).onFlashLoan(
|
||||
asset,
|
||||
amount,
|
||||
premium,
|
||||
callbackData
|
||||
);
|
||||
|
||||
require(result == CALLBACK_SUCCESS, "Callback failed");
|
||||
|
||||
// Repay flash loan
|
||||
IERC20(asset).safeApprove(aavePool, totalRepayment);
|
||||
emit FlashLoanRepaid(asset, totalRepayment);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Balancer flash loan callback
|
||||
*/
|
||||
function receiveFlashLoan(
|
||||
address[] memory tokens,
|
||||
uint256[] memory amounts,
|
||||
uint256[] memory feeAmounts,
|
||||
bytes memory userData
|
||||
) external {
|
||||
require(msg.sender == balancerVault, "Invalid caller");
|
||||
require(flashLoanState.inProgress, "Not in flash loan");
|
||||
require(tokens.length == 1, "Single asset only");
|
||||
|
||||
(address receiver, bytes memory callbackData) = abi.decode(userData, (address, bytes));
|
||||
|
||||
address asset = tokens[0];
|
||||
uint256 amount = amounts[0];
|
||||
uint256 fee = feeAmounts[0];
|
||||
|
||||
// Call receiver callback
|
||||
bytes32 result = IFlashLoanReceiver(receiver).onFlashLoan(
|
||||
asset,
|
||||
amount,
|
||||
fee,
|
||||
callbackData
|
||||
);
|
||||
|
||||
require(result == CALLBACK_SUCCESS, "Callback failed");
|
||||
|
||||
// Repay flash loan
|
||||
uint256 totalRepayment = amount + fee;
|
||||
IERC20(asset).safeApprove(balancerVault, totalRepayment);
|
||||
emit FlashLoanRepaid(asset, totalRepayment);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get available liquidity from Aave
|
||||
*/
|
||||
function getAvailableLiquidity(
|
||||
address asset,
|
||||
FlashLoanProvider provider
|
||||
) external view override returns (uint256) {
|
||||
if (provider == FlashLoanProvider.AAVE) {
|
||||
// Query Aave liquidity (simplified)
|
||||
// In production, would query Aave Pool's available liquidity
|
||||
return type(uint256).max; // Placeholder
|
||||
} else if (provider == FlashLoanProvider.BALANCER) {
|
||||
// Query Balancer liquidity
|
||||
return type(uint256).max; // Placeholder
|
||||
} else if (provider == FlashLoanProvider.DAI_FLASH) {
|
||||
// DAI flash mint has no limit
|
||||
return type(uint256).max;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get flash loan fee
|
||||
*/
|
||||
function getFlashLoanFee(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
FlashLoanProvider provider
|
||||
) external view override returns (uint256) {
|
||||
if (provider == FlashLoanProvider.AAVE) {
|
||||
// Aave v3: 0.05% premium
|
||||
return (amount * 5) / 10000;
|
||||
} else if (provider == FlashLoanProvider.BALANCER) {
|
||||
// Balancer: variable fee
|
||||
return (amount * 1) / 10000; // 0.01% placeholder
|
||||
} else if (provider == FlashLoanProvider.DAI_FLASH) {
|
||||
// DAI flash mint: 0% fee (plus gas cost)
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update provider addresses
|
||||
*/
|
||||
function setAavePool(address newPool) external onlyOwner {
|
||||
address oldPool = aavePool;
|
||||
aavePool = newPool;
|
||||
emit ProviderAddressUpdated(FlashLoanProvider.AAVE, oldPool, newPool);
|
||||
}
|
||||
|
||||
function setBalancerVault(address newVault) external onlyOwner {
|
||||
address oldVault = balancerVault;
|
||||
balancerVault = newVault;
|
||||
emit ProviderAddressUpdated(FlashLoanProvider.BALANCER, oldVault, newVault);
|
||||
}
|
||||
|
||||
function setDaiFlashMint(address newFlashMint) external onlyOwner {
|
||||
address oldFlashMint = daiFlashMint;
|
||||
daiFlashMint = newFlashMint;
|
||||
emit ProviderAddressUpdated(FlashLoanProvider.DAI_FLASH, oldFlashMint, newFlashMint);
|
||||
}
|
||||
}
|
||||
|
||||
482
contracts/core/RecursiveLeverageKernel.sol
Normal file
482
contracts/core/RecursiveLeverageKernel.sol
Normal file
@@ -0,0 +1,482 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
117
contracts/governance/ConfigRegistry.sol
Normal file
117
contracts/governance/ConfigRegistry.sol
Normal file
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../interfaces/IConfigRegistry.sol";
|
||||
|
||||
/**
|
||||
* @title ConfigRegistry
|
||||
* @notice Stores all system parameters and limits
|
||||
* @dev Central configuration registry with access control
|
||||
*/
|
||||
contract ConfigRegistry is IConfigRegistry, Ownable {
|
||||
// Constants
|
||||
uint256 private constant HF_SCALE = 1e18;
|
||||
uint256 private constant DEFAULT_MIN_HF = 1.05e18; // 1.05
|
||||
uint256 private constant DEFAULT_TARGET_HF = 1.20e18; // 1.20
|
||||
uint256 private constant DEFAULT_MAX_LOOPS = 5;
|
||||
|
||||
// Core parameters
|
||||
uint256 public override maxLoops = DEFAULT_MAX_LOOPS;
|
||||
uint256 public override minHealthFactor = DEFAULT_MIN_HF;
|
||||
uint256 public override targetHealthFactor = DEFAULT_TARGET_HF;
|
||||
|
||||
// Asset-specific limits
|
||||
mapping(address => uint256) public override maxFlashSize;
|
||||
mapping(address => bool) public override isAllowedAsset;
|
||||
|
||||
// Provider capacity caps
|
||||
mapping(bytes32 => uint256) public override providerCap;
|
||||
|
||||
// Parameter name constants (for events)
|
||||
bytes32 public constant PARAM_MAX_LOOPS = keccak256("MAX_LOOPS");
|
||||
bytes32 public constant PARAM_MIN_HF = keccak256("MIN_HF");
|
||||
bytes32 public constant PARAM_TARGET_HF = keccak256("TARGET_HF");
|
||||
bytes32 public constant PARAM_MAX_FLASH = keccak256("MAX_FLASH");
|
||||
bytes32 public constant PARAM_PROVIDER_CAP = keccak256("PROVIDER_CAP");
|
||||
|
||||
modifier validHealthFactor(uint256 hf) {
|
||||
require(hf >= 1e18, "HF must be >= 1.0");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {
|
||||
// Initialize with safe defaults
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum loops
|
||||
*/
|
||||
function setMaxLoops(uint256 newMaxLoops) external override onlyOwner {
|
||||
require(newMaxLoops > 0 && newMaxLoops <= 50, "Invalid max loops");
|
||||
uint256 oldValue = maxLoops;
|
||||
maxLoops = newMaxLoops;
|
||||
emit ParameterUpdated(PARAM_MAX_LOOPS, oldValue, newMaxLoops);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum flash size for an asset
|
||||
*/
|
||||
function setMaxFlashSize(address asset, uint256 newMaxFlash) external override onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
uint256 oldValue = maxFlashSize[asset];
|
||||
maxFlashSize[asset] = newMaxFlash;
|
||||
emit ParameterUpdated(keccak256(abi.encodePacked(PARAM_MAX_FLASH, asset)), oldValue, newMaxFlash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update minimum health factor
|
||||
*/
|
||||
function setMinHealthFactor(uint256 newMinHF) external override onlyOwner validHealthFactor(newMinHF) {
|
||||
require(newMinHF <= targetHealthFactor, "Min HF must be <= target HF");
|
||||
uint256 oldValue = minHealthFactor;
|
||||
minHealthFactor = newMinHF;
|
||||
emit ParameterUpdated(PARAM_MIN_HF, oldValue, newMinHF);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update target health factor
|
||||
*/
|
||||
function setTargetHealthFactor(uint256 newTargetHF) external override onlyOwner validHealthFactor(newTargetHF) {
|
||||
require(newTargetHF >= minHealthFactor, "Target HF must be >= min HF");
|
||||
uint256 oldValue = targetHealthFactor;
|
||||
targetHealthFactor = newTargetHF;
|
||||
emit ParameterUpdated(PARAM_TARGET_HF, oldValue, newTargetHF);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Add or remove allowed asset
|
||||
*/
|
||||
function setAllowedAsset(address asset, bool allowed) external override onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
bool oldValue = isAllowedAsset[asset];
|
||||
isAllowedAsset[asset] = allowed;
|
||||
emit ParameterUpdated(keccak256(abi.encodePacked("ALLOWED_ASSET", asset)), oldValue ? 1 : 0, allowed ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update provider capacity cap
|
||||
*/
|
||||
function setProviderCap(bytes32 provider, uint256 newCap) external override onlyOwner {
|
||||
require(provider != bytes32(0), "Invalid provider");
|
||||
uint256 oldValue = providerCap[provider];
|
||||
providerCap[provider] = newCap;
|
||||
emit ParameterUpdated(keccak256(abi.encodePacked(PARAM_PROVIDER_CAP, provider)), oldValue, newCap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Batch update allowed assets
|
||||
*/
|
||||
function batchSetAllowedAssets(address[] calldata assets, bool[] calldata allowed) external onlyOwner {
|
||||
require(assets.length == allowed.length, "Array length mismatch");
|
||||
for (uint256 i = 0; i < assets.length; i++) {
|
||||
setAllowedAsset(assets[i], allowed[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
217
contracts/governance/GovernanceGuard.sol
Normal file
217
contracts/governance/GovernanceGuard.sol
Normal file
@@ -0,0 +1,217 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../interfaces/IPolicyEngine.sol";
|
||||
import "../interfaces/IConfigRegistry.sol";
|
||||
import "../interfaces/IVault.sol";
|
||||
|
||||
/**
|
||||
* @title GovernanceGuard
|
||||
* @notice Enforces invariants and policy checks before execution
|
||||
* @dev Acts as the final gatekeeper for all system actions
|
||||
*/
|
||||
contract GovernanceGuard is Ownable {
|
||||
IPolicyEngine public policyEngine;
|
||||
IConfigRegistry public configRegistry;
|
||||
IVault public vault;
|
||||
|
||||
// Strategy throttling
|
||||
struct ThrottleConfig {
|
||||
uint256 dailyCap;
|
||||
uint256 monthlyCap;
|
||||
uint256 dailyCount;
|
||||
uint256 monthlyCount;
|
||||
uint256 lastDailyReset;
|
||||
uint256 lastMonthlyReset;
|
||||
}
|
||||
|
||||
mapping(bytes32 => ThrottleConfig) private strategyThrottles;
|
||||
|
||||
event InvariantCheckFailed(string reason);
|
||||
event PolicyCheckFailed(string reason);
|
||||
event ThrottleExceeded(string strategy, string period);
|
||||
|
||||
modifier onlyVault() {
|
||||
require(msg.sender == address(vault), "Only vault");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(
|
||||
address _policyEngine,
|
||||
address _configRegistry,
|
||||
address _vault,
|
||||
address initialOwner
|
||||
) Ownable(initialOwner) {
|
||||
require(_policyEngine != address(0), "Invalid policy engine");
|
||||
require(_configRegistry != address(0), "Invalid config registry");
|
||||
require(_vault != address(0), "Invalid vault");
|
||||
|
||||
policyEngine = IPolicyEngine(_policyEngine);
|
||||
configRegistry = IConfigRegistry(_configRegistry);
|
||||
vault = IVault(_vault);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify invariants before action
|
||||
* @param actionType Action type identifier
|
||||
* @param actionData Action-specific data
|
||||
* @return success True if all checks pass
|
||||
*/
|
||||
function verifyInvariants(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view returns (bool success) {
|
||||
// Policy check
|
||||
(bool policyAllowed, string memory policyReason) = policyEngine.evaluateAll(actionType, actionData);
|
||||
if (!policyAllowed) {
|
||||
return false; // Would emit event in actual execution
|
||||
}
|
||||
|
||||
// Position invariant check (for amortization actions)
|
||||
if (actionType == keccak256("AMORTIZATION")) {
|
||||
// Decode and verify position improvement
|
||||
// This would decode the expected position changes and verify
|
||||
// For now, return true - actual implementation would check
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check and enforce invariants (with revert)
|
||||
* @param actionType Action type
|
||||
* @param actionData Action data
|
||||
*/
|
||||
function enforceInvariants(bytes32 actionType, bytes memory actionData) external {
|
||||
// Policy check
|
||||
(bool policyAllowed, string memory policyReason) = policyEngine.evaluateAll(actionType, actionData);
|
||||
if (!policyAllowed) {
|
||||
emit PolicyCheckFailed(policyReason);
|
||||
revert(string(abi.encodePacked("Policy check failed: ", policyReason)));
|
||||
}
|
||||
|
||||
// Throttle check
|
||||
if (!checkThrottle(actionType)) {
|
||||
emit ThrottleExceeded(_bytes32ToString(actionType), "daily or monthly");
|
||||
revert("Strategy throttle exceeded");
|
||||
}
|
||||
|
||||
// Record throttle usage
|
||||
recordThrottleUsage(actionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Verify position improved (invariant check)
|
||||
* @param collateralBefore Previous collateral value
|
||||
* @param debtBefore Previous debt value
|
||||
* @param healthFactorBefore Previous health factor
|
||||
*/
|
||||
function verifyPositionImproved(
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 healthFactorBefore
|
||||
) external view returns (bool) {
|
||||
return vault.verifyPositionImproved(collateralBefore, debtBefore, healthFactorBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check throttle limits
|
||||
*/
|
||||
function checkThrottle(bytes32 strategy) public view returns (bool) {
|
||||
ThrottleConfig storage throttle = strategyThrottles[strategy];
|
||||
|
||||
// Reset if needed
|
||||
uint256 currentDailyCount = throttle.dailyCount;
|
||||
uint256 currentMonthlyCount = throttle.monthlyCount;
|
||||
|
||||
if (block.timestamp - throttle.lastDailyReset >= 1 days) {
|
||||
currentDailyCount = 0;
|
||||
}
|
||||
if (block.timestamp - throttle.lastMonthlyReset >= 30 days) {
|
||||
currentMonthlyCount = 0;
|
||||
}
|
||||
|
||||
// Check limits
|
||||
if (throttle.dailyCap > 0 && currentDailyCount >= throttle.dailyCap) {
|
||||
return false;
|
||||
}
|
||||
if (throttle.monthlyCap > 0 && currentMonthlyCount >= throttle.monthlyCap) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record throttle usage
|
||||
*/
|
||||
function recordThrottleUsage(bytes32 strategy) internal {
|
||||
ThrottleConfig storage throttle = strategyThrottles[strategy];
|
||||
|
||||
// Reset daily if needed
|
||||
if (block.timestamp - throttle.lastDailyReset >= 1 days) {
|
||||
throttle.dailyCount = 0;
|
||||
throttle.lastDailyReset = block.timestamp;
|
||||
}
|
||||
|
||||
// Reset monthly if needed
|
||||
if (block.timestamp - throttle.lastMonthlyReset >= 30 days) {
|
||||
throttle.monthlyCount = 0;
|
||||
throttle.lastMonthlyReset = block.timestamp;
|
||||
}
|
||||
|
||||
throttle.dailyCount++;
|
||||
throttle.monthlyCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Configure throttle for a strategy
|
||||
*/
|
||||
function setThrottle(
|
||||
bytes32 strategy,
|
||||
uint256 dailyCap,
|
||||
uint256 monthlyCap
|
||||
) external onlyOwner {
|
||||
strategyThrottles[strategy] = ThrottleConfig({
|
||||
dailyCap: dailyCap,
|
||||
monthlyCap: monthlyCap,
|
||||
dailyCount: 0,
|
||||
monthlyCount: 0,
|
||||
lastDailyReset: block.timestamp,
|
||||
lastMonthlyReset: block.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update policy engine
|
||||
*/
|
||||
function setPolicyEngine(address newPolicyEngine) external onlyOwner {
|
||||
require(newPolicyEngine != address(0), "Invalid policy engine");
|
||||
policyEngine = IPolicyEngine(newPolicyEngine);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update config registry
|
||||
*/
|
||||
function setConfigRegistry(address newConfigRegistry) external onlyOwner {
|
||||
require(newConfigRegistry != address(0), "Invalid config registry");
|
||||
configRegistry = IConfigRegistry(newConfigRegistry);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Helper to convert bytes32 to string
|
||||
*/
|
||||
function _bytes32ToString(bytes32 _bytes32) private pure returns (string memory) {
|
||||
uint8 i = 0;
|
||||
while (i < 32 && _bytes32[i] != 0) {
|
||||
i++;
|
||||
}
|
||||
bytes memory bytesArray = new bytes(i);
|
||||
for (i = 0; i < 32 && _bytes32[i] != 0; i++) {
|
||||
bytesArray[i] = _bytes32[i];
|
||||
}
|
||||
return string(bytesArray);
|
||||
}
|
||||
}
|
||||
|
||||
129
contracts/governance/PolicyEngine.sol
Normal file
129
contracts/governance/PolicyEngine.sol
Normal file
@@ -0,0 +1,129 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../interfaces/IPolicyEngine.sol";
|
||||
import "../interfaces/IPolicyModule.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyEngine
|
||||
* @notice Aggregates policy decisions from multiple modules
|
||||
* @dev All registered modules must approve an action for it to be allowed
|
||||
*/
|
||||
contract PolicyEngine is IPolicyEngine, Ownable {
|
||||
// Registered policy modules
|
||||
address[] private policyModules;
|
||||
mapping(address => bool) private isRegisteredModule;
|
||||
|
||||
modifier onlyRegistered(address module) {
|
||||
require(isRegisteredModule[module], "Module not registered");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {}
|
||||
|
||||
/**
|
||||
* @notice Register a policy module
|
||||
*/
|
||||
function registerPolicyModule(address module) external override onlyOwner {
|
||||
require(module != address(0), "Invalid module");
|
||||
require(!isRegisteredModule[module], "Module already registered");
|
||||
|
||||
// Verify it implements IPolicyModule
|
||||
try IPolicyModule(module).name() returns (string memory) {
|
||||
// Module is valid
|
||||
} catch {
|
||||
revert("Invalid policy module");
|
||||
}
|
||||
|
||||
policyModules.push(module);
|
||||
isRegisteredModule[module] = true;
|
||||
|
||||
emit PolicyModuleRegistered(module, IPolicyModule(module).name());
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Unregister a policy module
|
||||
*/
|
||||
function unregisterPolicyModule(address module) external override onlyOwner onlyRegistered(module) {
|
||||
// Remove from array
|
||||
for (uint256 i = 0; i < policyModules.length; i++) {
|
||||
if (policyModules[i] == module) {
|
||||
policyModules[i] = policyModules[policyModules.length - 1];
|
||||
policyModules.pop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delete isRegisteredModule[module];
|
||||
emit PolicyModuleUnregistered(module);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Evaluate all registered policy modules
|
||||
* @return allowed True if ALL modules allow the action
|
||||
* @return reason Reason from first denying module
|
||||
*/
|
||||
function evaluateAll(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override returns (bool allowed, string memory reason) {
|
||||
// If no modules registered, allow by default
|
||||
if (policyModules.length == 0) {
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
// Check all modules
|
||||
for (uint256 i = 0; i < policyModules.length; i++) {
|
||||
address module = policyModules[i];
|
||||
|
||||
// Skip if module is disabled
|
||||
try IPolicyModule(module).isEnabled() returns (bool enabled) {
|
||||
if (!enabled) {
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
continue; // Skip if check fails
|
||||
}
|
||||
|
||||
// Get decision from module
|
||||
IPolicyModule.PolicyDecision memory decision;
|
||||
try IPolicyModule(module).evaluate(actionType, actionData) returns (IPolicyModule.PolicyDecision memory d) {
|
||||
decision = d;
|
||||
} catch {
|
||||
// If evaluation fails, deny for safety
|
||||
return (false, "Policy evaluation failed");
|
||||
}
|
||||
|
||||
// If any module denies, return denial
|
||||
if (!decision.allowed) {
|
||||
return (false, decision.reason);
|
||||
}
|
||||
}
|
||||
|
||||
// All modules allowed
|
||||
return (true, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get all registered policy modules
|
||||
*/
|
||||
function getPolicyModules() external view override returns (address[] memory) {
|
||||
return policyModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check if a module is registered
|
||||
*/
|
||||
function isRegistered(address module) external view override returns (bool) {
|
||||
return isRegisteredModule[module];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get number of registered modules
|
||||
*/
|
||||
function getModuleCount() external view returns (uint256) {
|
||||
return policyModules.length;
|
||||
}
|
||||
}
|
||||
|
||||
187
contracts/governance/policies/PolicyFlashVolume.sol
Normal file
187
contracts/governance/policies/PolicyFlashVolume.sol
Normal file
@@ -0,0 +1,187 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "../IPolicyModule.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyFlashVolume
|
||||
* @notice Policy module that limits flash loan volume per time period
|
||||
* @dev Prevents excessive flash loan usage
|
||||
*/
|
||||
contract PolicyFlashVolume is IPolicyModule, Ownable {
|
||||
string public constant override name = "FlashVolume";
|
||||
|
||||
bool private _enabled = true;
|
||||
|
||||
// Time period for volume tracking (e.g., 1 day = 86400 seconds)
|
||||
uint256 public periodDuration = 1 days;
|
||||
|
||||
// Volume limits per period
|
||||
mapping(address => uint256) public assetVolumeLimit; // Per asset limit
|
||||
uint256 public globalVolumeLimit = type(uint256).max; // Global limit
|
||||
|
||||
// Volume tracking
|
||||
struct VolumePeriod {
|
||||
uint256 volume;
|
||||
uint256 startTime;
|
||||
uint256 endTime;
|
||||
}
|
||||
|
||||
mapping(address => mapping(uint256 => VolumePeriod)) private assetVolumes; // asset => periodId => VolumePeriod
|
||||
mapping(uint256 => VolumePeriod) private globalVolumes; // periodId => VolumePeriod
|
||||
|
||||
event VolumeLimitUpdated(address indexed asset, uint256 oldLimit, uint256 newLimit);
|
||||
event GlobalVolumeLimitUpdated(uint256 oldLimit, uint256 newLimit);
|
||||
event PeriodDurationUpdated(uint256 oldDuration, uint256 newDuration);
|
||||
|
||||
modifier onlyEnabled() {
|
||||
require(_enabled, "Policy disabled");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {}
|
||||
|
||||
/**
|
||||
* @notice Check if module is enabled
|
||||
*/
|
||||
function isEnabled() external view override returns (bool) {
|
||||
return _enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable or disable the module
|
||||
*/
|
||||
function setEnabled(bool enabled) external override onlyOwner {
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Evaluate policy for proposed action
|
||||
* @param actionType Action type (FLASH_LOAN, etc.)
|
||||
* @param actionData Encoded action data: (asset, amount)
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override onlyEnabled returns (PolicyDecision memory) {
|
||||
if (actionType != keccak256("FLASH_LOAN")) {
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
(address asset, uint256 amount) = abi.decode(actionData, (address, uint256));
|
||||
|
||||
// Get current period
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
|
||||
// Check asset-specific limit
|
||||
if (assetVolumeLimit[asset] > 0) {
|
||||
VolumePeriod storage assetPeriod = assetVolumes[asset][periodId];
|
||||
uint256 newVolume = assetPeriod.volume + amount;
|
||||
|
||||
if (newVolume > assetVolumeLimit[asset]) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Asset volume limit exceeded"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check global limit
|
||||
if (globalVolumeLimit < type(uint256).max) {
|
||||
VolumePeriod storage globalPeriod = globalVolumes[periodId];
|
||||
uint256 newVolume = globalPeriod.volume + amount;
|
||||
|
||||
if (newVolume > globalVolumeLimit) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Global volume limit exceeded"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record flash loan volume
|
||||
*/
|
||||
function recordVolume(address asset, uint256 amount) external {
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
|
||||
// Update asset volume
|
||||
VolumePeriod storage assetPeriod = assetVolumes[asset][periodId];
|
||||
if (assetPeriod.startTime == 0) {
|
||||
assetPeriod.startTime = block.timestamp;
|
||||
assetPeriod.endTime = block.timestamp + periodDuration;
|
||||
}
|
||||
assetPeriod.volume += amount;
|
||||
|
||||
// Update global volume
|
||||
VolumePeriod storage globalPeriod = globalVolumes[periodId];
|
||||
if (globalPeriod.startTime == 0) {
|
||||
globalPeriod.startTime = block.timestamp;
|
||||
globalPeriod.endTime = block.timestamp + periodDuration;
|
||||
}
|
||||
globalPeriod.volume += amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set volume limit for an asset
|
||||
*/
|
||||
function setAssetVolumeLimit(address asset, uint256 limit) external onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
uint256 oldLimit = assetVolumeLimit[asset];
|
||||
assetVolumeLimit[asset] = limit;
|
||||
emit VolumeLimitUpdated(asset, oldLimit, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set global volume limit
|
||||
*/
|
||||
function setGlobalVolumeLimit(uint256 limit) external onlyOwner {
|
||||
uint256 oldLimit = globalVolumeLimit;
|
||||
globalVolumeLimit = limit;
|
||||
emit GlobalVolumeLimitUpdated(oldLimit, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Set period duration
|
||||
*/
|
||||
function setPeriodDuration(uint256 duration) external onlyOwner {
|
||||
require(duration > 0, "Invalid duration");
|
||||
uint256 oldDuration = periodDuration;
|
||||
periodDuration = duration;
|
||||
emit PeriodDurationUpdated(oldDuration, duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current period ID
|
||||
*/
|
||||
function getCurrentPeriodId() public view returns (uint256) {
|
||||
return block.timestamp / periodDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current period volume for an asset
|
||||
*/
|
||||
function getAssetPeriodVolume(address asset) external view returns (uint256) {
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
return assetVolumes[asset][periodId].volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get current period global volume
|
||||
*/
|
||||
function getGlobalPeriodVolume() external view returns (uint256) {
|
||||
uint256 periodId = getCurrentPeriodId();
|
||||
return globalVolumes[periodId].volume;
|
||||
}
|
||||
}
|
||||
|
||||
188
contracts/governance/policies/PolicyHFTrend.sol
Normal file
188
contracts/governance/policies/PolicyHFTrend.sol
Normal file
@@ -0,0 +1,188 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "../IPolicyModule.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyHFTrend
|
||||
* @notice Policy module that monitors health factor trends
|
||||
* @dev Prevents actions that would worsen health factor trajectory
|
||||
*/
|
||||
contract PolicyHFTrend is IPolicyModule, Ownable {
|
||||
string public constant override name = "HealthFactorTrend";
|
||||
|
||||
bool private _enabled = true;
|
||||
uint256 private constant HF_SCALE = 1e18;
|
||||
uint256 private minHFImprovement = 0.01e18; // 1% minimum improvement
|
||||
uint256 private minHFThreshold = 1.05e18; // 1.05 minimum HF
|
||||
|
||||
// Track HF history for trend analysis
|
||||
struct HFHistory {
|
||||
uint256[] values;
|
||||
uint256[] timestamps;
|
||||
uint256 maxHistoryLength;
|
||||
}
|
||||
|
||||
mapping(address => HFHistory) private vaultHistory;
|
||||
|
||||
event HFThresholdUpdated(uint256 oldThreshold, uint256 newThreshold);
|
||||
event MinHFImprovementUpdated(uint256 oldMin, uint256 newMin);
|
||||
|
||||
modifier onlyEnabled() {
|
||||
require(_enabled, "Policy disabled");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {}
|
||||
|
||||
/**
|
||||
* @notice Check if module is enabled
|
||||
*/
|
||||
function isEnabled() external view override returns (bool) {
|
||||
return _enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable or disable the module
|
||||
*/
|
||||
function setEnabled(bool enabled) external override onlyOwner {
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Evaluate policy for proposed action
|
||||
* @param actionType Action type (AMORTIZATION, LEVERAGE, etc.)
|
||||
* @param actionData Encoded action data: (vault, hfBefore, hfAfter, collateralChange, debtChange)
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override onlyEnabled returns (PolicyDecision memory) {
|
||||
// Decode action data
|
||||
(
|
||||
address vault,
|
||||
uint256 hfBefore,
|
||||
uint256 hfAfter,
|
||||
int256 collateralChange,
|
||||
int256 debtChange
|
||||
) = abi.decode(actionData, (address, uint256, uint256, int256, int256));
|
||||
|
||||
// Check minimum HF threshold
|
||||
if (hfAfter < minHFThreshold) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "HF below minimum threshold"
|
||||
});
|
||||
}
|
||||
|
||||
// For amortization actions, require improvement
|
||||
if (actionType == keccak256("AMORTIZATION")) {
|
||||
if (hfAfter <= hfBefore) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "HF must improve"
|
||||
});
|
||||
}
|
||||
|
||||
uint256 hfImprovement = hfAfter > hfBefore ? hfAfter - hfBefore : 0;
|
||||
if (hfImprovement < minHFImprovement) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "HF improvement too small"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check trend (require improving trajectory)
|
||||
if (hfAfter < hfBefore) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "HF trend declining"
|
||||
});
|
||||
}
|
||||
|
||||
// Check that collateral increases or debt decreases (amortization requirement)
|
||||
if (actionType == keccak256("AMORTIZATION")) {
|
||||
if (collateralChange <= 0 && debtChange >= 0) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Amortization requires collateral increase or debt decrease"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update minimum HF threshold
|
||||
*/
|
||||
function setMinHFThreshold(uint256 newThreshold) external onlyOwner {
|
||||
require(newThreshold >= 1e18, "HF must be >= 1.0");
|
||||
uint256 oldThreshold = minHFThreshold;
|
||||
minHFThreshold = newThreshold;
|
||||
emit HFThresholdUpdated(oldThreshold, newThreshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update minimum HF improvement required
|
||||
*/
|
||||
function setMinHFImprovement(uint256 newMinImprovement) external onlyOwner {
|
||||
require(newMinImprovement <= HF_SCALE, "Invalid improvement");
|
||||
uint256 oldMin = minHFImprovement;
|
||||
minHFImprovement = newMinImprovement;
|
||||
emit MinHFImprovementUpdated(oldMin, newMinImprovement);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get minimum HF threshold
|
||||
*/
|
||||
function getMinHFThreshold() external view returns (uint256) {
|
||||
return minHFThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record HF value for trend tracking
|
||||
*/
|
||||
function recordHF(address vault, uint256 hf) external {
|
||||
HFHistory storage history = vaultHistory[vault];
|
||||
if (history.maxHistoryLength == 0) {
|
||||
history.maxHistoryLength = 10; // Default max history
|
||||
}
|
||||
|
||||
history.values.push(hf);
|
||||
history.timestamps.push(block.timestamp);
|
||||
|
||||
// Limit history length
|
||||
if (history.values.length > history.maxHistoryLength) {
|
||||
// Remove oldest entry (shift array)
|
||||
for (uint256 i = 0; i < history.values.length - 1; i++) {
|
||||
history.values[i] = history.values[i + 1];
|
||||
history.timestamps[i] = history.timestamps[i + 1];
|
||||
}
|
||||
history.values.pop();
|
||||
history.timestamps.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get HF trend (slope)
|
||||
* @return trend Positive = improving, negative = declining
|
||||
*/
|
||||
function getHFTrend(address vault) external view returns (int256 trend) {
|
||||
HFHistory storage history = vaultHistory[vault];
|
||||
if (history.values.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint256 latest = history.values[history.values.length - 1];
|
||||
uint256 previous = history.values[history.values.length - 2];
|
||||
|
||||
return int256(latest) - int256(previous);
|
||||
}
|
||||
}
|
||||
|
||||
141
contracts/governance/policies/PolicyLiquiditySpread.sol
Normal file
141
contracts/governance/policies/PolicyLiquiditySpread.sol
Normal file
@@ -0,0 +1,141 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "../IPolicyModule.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyLiquiditySpread
|
||||
* @notice Policy module that validates liquidity spreads
|
||||
* @dev Ensures sufficient liquidity depth for operations
|
||||
*/
|
||||
contract PolicyLiquiditySpread is IPolicyModule, Ownable {
|
||||
string public constant override name = "LiquiditySpread";
|
||||
|
||||
bool private _enabled = true;
|
||||
|
||||
// Maximum acceptable spread (basis points, e.g., 50 = 0.5%)
|
||||
uint256 public maxSpreadBps = 50; // 0.5%
|
||||
uint256 private constant BPS_SCALE = 10000;
|
||||
|
||||
// Minimum liquidity depth required (in USD, scaled by 1e8)
|
||||
mapping(address => uint256) public minLiquidityDepth;
|
||||
|
||||
// Interface for checking liquidity
|
||||
interface ILiquidityChecker {
|
||||
function getLiquidityDepth(address asset) external view returns (uint256);
|
||||
function getSpread(address asset, uint256 amount) external view returns (uint256);
|
||||
}
|
||||
|
||||
ILiquidityChecker public liquidityChecker;
|
||||
|
||||
event MaxSpreadUpdated(uint256 oldSpread, uint256 newSpread);
|
||||
event MinLiquidityDepthUpdated(address indexed asset, uint256 oldDepth, uint256 newDepth);
|
||||
event LiquidityCheckerUpdated(address oldChecker, address newChecker);
|
||||
|
||||
modifier onlyEnabled() {
|
||||
require(_enabled, "Policy disabled");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner, address _liquidityChecker) Ownable(initialOwner) {
|
||||
liquidityChecker = ILiquidityChecker(_liquidityChecker);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Check if module is enabled
|
||||
*/
|
||||
function isEnabled() external view override returns (bool) {
|
||||
return _enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable or disable the module
|
||||
*/
|
||||
function setEnabled(bool enabled) external override onlyOwner {
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Evaluate policy for proposed action
|
||||
* @param actionType Action type (SWAP, FLASH_LOAN, etc.)
|
||||
* @param actionData Encoded action data: (asset, amount, spreadBps, liquidityDepth)
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override onlyEnabled returns (PolicyDecision memory) {
|
||||
// For swaps and flash loans, check liquidity
|
||||
if (actionType != keccak256("SWAP") && actionType != keccak256("FLASH_LOAN")) {
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
(address asset, uint256 amount, uint256 spreadBps, uint256 liquidityDepth) =
|
||||
abi.decode(actionData, (address, uint256, uint256, uint256));
|
||||
|
||||
// Check spread
|
||||
if (spreadBps > maxSpreadBps) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Spread too high"
|
||||
});
|
||||
}
|
||||
|
||||
// Check minimum liquidity depth
|
||||
uint256 requiredDepth = minLiquidityDepth[asset];
|
||||
if (requiredDepth > 0 && liquidityDepth < requiredDepth) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Insufficient liquidity depth"
|
||||
});
|
||||
}
|
||||
|
||||
// Additional check: ensure liquidity depth is sufficient for the amount
|
||||
// Rule: liquidity should be at least 2x the operation amount
|
||||
if (liquidityDepth < amount * 2) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Liquidity depth insufficient for operation size"
|
||||
});
|
||||
}
|
||||
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum spread
|
||||
*/
|
||||
function setMaxSpread(uint256 newSpreadBps) external onlyOwner {
|
||||
require(newSpreadBps <= BPS_SCALE, "Invalid spread");
|
||||
uint256 oldSpread = maxSpreadBps;
|
||||
maxSpreadBps = newSpreadBps;
|
||||
emit MaxSpreadUpdated(oldSpread, newSpreadBps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update minimum liquidity depth for an asset
|
||||
*/
|
||||
function setMinLiquidityDepth(address asset, uint256 depth) external onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
uint256 oldDepth = minLiquidityDepth[asset];
|
||||
minLiquidityDepth[asset] = depth;
|
||||
emit MinLiquidityDepthUpdated(asset, oldDepth, depth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update liquidity checker contract
|
||||
*/
|
||||
function setLiquidityChecker(address newChecker) external onlyOwner {
|
||||
require(newChecker != address(0), "Invalid checker");
|
||||
address oldChecker = address(liquidityChecker);
|
||||
liquidityChecker = ILiquidityChecker(newChecker);
|
||||
emit LiquidityCheckerUpdated(oldChecker, newChecker);
|
||||
}
|
||||
}
|
||||
|
||||
200
contracts/governance/policies/PolicyProviderConcentration.sol
Normal file
200
contracts/governance/policies/PolicyProviderConcentration.sol
Normal file
@@ -0,0 +1,200 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "../IPolicyModule.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../../interfaces/IFlashLoanRouter.sol";
|
||||
|
||||
/**
|
||||
* @title PolicyProviderConcentration
|
||||
* @notice Policy module that prevents over-concentration in single providers
|
||||
* @dev Ensures diversification across flash loan providers
|
||||
*/
|
||||
contract PolicyProviderConcentration is IPolicyModule, Ownable {
|
||||
string public constant override name = "ProviderConcentration";
|
||||
|
||||
bool private _enabled = true;
|
||||
|
||||
// Maximum percentage of total flash loans from a single provider (basis points)
|
||||
uint256 public maxProviderConcentrationBps = 5000; // 50%
|
||||
uint256 private constant BPS_SCALE = 10000;
|
||||
|
||||
// Time window for concentration tracking
|
||||
uint256 public trackingWindow = 7 days;
|
||||
|
||||
// Provider usage tracking
|
||||
struct ProviderUsage {
|
||||
uint256 totalVolume;
|
||||
uint256 lastResetTime;
|
||||
mapping(IFlashLoanRouter.FlashLoanProvider => uint256) providerVolumes;
|
||||
}
|
||||
|
||||
mapping(address => ProviderUsage) private vaultProviderUsage; // vault => ProviderUsage
|
||||
ProviderUsage private globalProviderUsage;
|
||||
|
||||
event MaxConcentrationUpdated(uint256 oldMax, uint256 newMax);
|
||||
event TrackingWindowUpdated(uint256 oldWindow, uint256 newWindow);
|
||||
|
||||
modifier onlyEnabled() {
|
||||
require(_enabled, "Policy disabled");
|
||||
_;
|
||||
}
|
||||
|
||||
constructor(address initialOwner) Ownable(initialOwner) {}
|
||||
|
||||
/**
|
||||
* @notice Check if module is enabled
|
||||
*/
|
||||
function isEnabled() external view override returns (bool) {
|
||||
return _enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable or disable the module
|
||||
*/
|
||||
function setEnabled(bool enabled) external override onlyOwner {
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Evaluate policy for proposed action
|
||||
* @param actionType Action type (FLASH_LOAN, etc.)
|
||||
* @param actionData Encoded action data: (vault, asset, amount, provider)
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view override onlyEnabled returns (PolicyDecision memory) {
|
||||
if (actionType != keccak256("FLASH_LOAN")) {
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
(
|
||||
address vault,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
IFlashLoanRouter.FlashLoanProvider provider
|
||||
) = abi.decode(actionData, (address, address, uint256, IFlashLoanRouter.FlashLoanProvider));
|
||||
|
||||
// Reset usage if window expired
|
||||
ProviderUsage storage vaultUsage = vaultProviderUsage[vault];
|
||||
if (block.timestamp - vaultUsage.lastResetTime > trackingWindow) {
|
||||
// Would reset in actual implementation, but for evaluation assume fresh window
|
||||
vaultUsage = globalProviderUsage; // Use global as proxy for "reset" state
|
||||
}
|
||||
|
||||
// Calculate new provider volume
|
||||
uint256 newProviderVolume = vaultUsage.providerVolumes[provider] + amount;
|
||||
uint256 newTotalVolume = vaultUsage.totalVolume + amount;
|
||||
|
||||
if (newTotalVolume > 0) {
|
||||
uint256 newConcentration = (newProviderVolume * BPS_SCALE) / newTotalVolume;
|
||||
|
||||
if (newConcentration > maxProviderConcentrationBps) {
|
||||
return PolicyDecision({
|
||||
allowed: false,
|
||||
reason: "Provider concentration limit exceeded"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyDecision({
|
||||
allowed: true,
|
||||
reason: ""
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Record flash loan usage
|
||||
*/
|
||||
function recordUsage(
|
||||
address vault,
|
||||
address asset,
|
||||
uint256 amount,
|
||||
IFlashLoanRouter.FlashLoanProvider provider
|
||||
) external {
|
||||
// Reset if window expired
|
||||
ProviderUsage storage vaultUsage = vaultProviderUsage[vault];
|
||||
if (block.timestamp - vaultUsage.lastResetTime > trackingWindow) {
|
||||
_resetUsage(vault);
|
||||
vaultUsage = vaultProviderUsage[vault];
|
||||
}
|
||||
|
||||
// Update usage
|
||||
vaultUsage.providerVolumes[provider] += amount;
|
||||
vaultUsage.totalVolume += amount;
|
||||
|
||||
// Update global usage
|
||||
ProviderUsage storage global = globalProviderUsage;
|
||||
if (block.timestamp - global.lastResetTime > trackingWindow) {
|
||||
_resetGlobalUsage();
|
||||
global = globalProviderUsage;
|
||||
}
|
||||
global.providerVolumes[provider] += amount;
|
||||
global.totalVolume += amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Reset usage for a vault
|
||||
*/
|
||||
function _resetUsage(address vault) internal {
|
||||
ProviderUsage storage usage = vaultProviderUsage[vault];
|
||||
usage.totalVolume = 0;
|
||||
usage.lastResetTime = block.timestamp;
|
||||
|
||||
// Reset all provider volumes
|
||||
for (uint256 i = 0; i <= uint256(IFlashLoanRouter.FlashLoanProvider.DAI_FLASH); i++) {
|
||||
usage.providerVolumes[IFlashLoanRouter.FlashLoanProvider(i)] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Reset global usage
|
||||
*/
|
||||
function _resetGlobalUsage() internal {
|
||||
globalProviderUsage.totalVolume = 0;
|
||||
globalProviderUsage.lastResetTime = block.timestamp;
|
||||
|
||||
for (uint256 i = 0; i <= uint256(IFlashLoanRouter.FlashLoanProvider.DAI_FLASH); i++) {
|
||||
globalProviderUsage.providerVolumes[IFlashLoanRouter.FlashLoanProvider(i)] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update maximum provider concentration
|
||||
*/
|
||||
function setMaxConcentration(uint256 newMaxBps) external onlyOwner {
|
||||
require(newMaxBps <= BPS_SCALE, "Invalid concentration");
|
||||
uint256 oldMax = maxProviderConcentrationBps;
|
||||
maxProviderConcentrationBps = newMaxBps;
|
||||
emit MaxConcentrationUpdated(oldMax, newMaxBps);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update tracking window
|
||||
*/
|
||||
function setTrackingWindow(uint256 newWindow) external onlyOwner {
|
||||
require(newWindow > 0, "Invalid window");
|
||||
uint256 oldWindow = trackingWindow;
|
||||
trackingWindow = newWindow;
|
||||
emit TrackingWindowUpdated(oldWindow, newWindow);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get provider concentration for a vault
|
||||
*/
|
||||
function getProviderConcentration(
|
||||
address vault,
|
||||
IFlashLoanRouter.FlashLoanProvider provider
|
||||
) external view returns (uint256 concentrationBps) {
|
||||
ProviderUsage storage usage = vaultProviderUsage[vault];
|
||||
if (usage.totalVolume == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (usage.providerVolumes[provider] * BPS_SCALE) / usage.totalVolume;
|
||||
}
|
||||
}
|
||||
|
||||
100
contracts/interfaces/IConfigRegistry.sol
Normal file
100
contracts/interfaces/IConfigRegistry.sol
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/**
|
||||
* @title IConfigRegistry
|
||||
* @notice Interface for configuration registry
|
||||
* @dev Stores all system parameters and limits
|
||||
*/
|
||||
interface IConfigRegistry {
|
||||
/**
|
||||
* @notice Emitted when a parameter is updated
|
||||
* @param param Parameter name (encoded as bytes32)
|
||||
* @param oldValue Previous value
|
||||
* @param newValue New value
|
||||
*/
|
||||
event ParameterUpdated(
|
||||
bytes32 indexed param,
|
||||
uint256 oldValue,
|
||||
uint256 newValue
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Get maximum number of recursive loops
|
||||
* @return maxLoops Maximum loops allowed
|
||||
*/
|
||||
function getMaxLoops() external view returns (uint256 maxLoops);
|
||||
|
||||
/**
|
||||
* @notice Get maximum flash loan size for an asset
|
||||
* @param asset Asset address
|
||||
* @return maxFlash Maximum flash loan size
|
||||
*/
|
||||
function getMaxFlashSize(address asset) external view returns (uint256 maxFlash);
|
||||
|
||||
/**
|
||||
* @notice Get minimum health factor threshold
|
||||
* @return minHF Minimum health factor (scaled by 1e18)
|
||||
*/
|
||||
function getMinHealthFactor() external view returns (uint256 minHF);
|
||||
|
||||
/**
|
||||
* @notice Get target health factor
|
||||
* @return targetHF Target health factor (scaled by 1e18)
|
||||
*/
|
||||
function getTargetHealthFactor() external view returns (uint256 targetHF);
|
||||
|
||||
/**
|
||||
* @notice Check if an asset is allowed
|
||||
* @param asset Asset address
|
||||
* @return allowed True if asset is allowed
|
||||
*/
|
||||
function isAllowedAsset(address asset) external view returns (bool allowed);
|
||||
|
||||
/**
|
||||
* @notice Get provider capacity cap
|
||||
* @param provider Provider identifier (encoded as bytes32)
|
||||
* @return cap Capacity cap
|
||||
*/
|
||||
function getProviderCap(bytes32 provider) external view returns (uint256 cap);
|
||||
|
||||
/**
|
||||
* @notice Update maximum loops
|
||||
* @param newMaxLoops New maximum loops
|
||||
*/
|
||||
function setMaxLoops(uint256 newMaxLoops) external;
|
||||
|
||||
/**
|
||||
* @notice Update maximum flash size for an asset
|
||||
* @param asset Asset address
|
||||
* @param newMaxFlash New maximum flash size
|
||||
*/
|
||||
function setMaxFlashSize(address asset, uint256 newMaxFlash) external;
|
||||
|
||||
/**
|
||||
* @notice Update minimum health factor
|
||||
* @param newMinHF New minimum health factor (scaled by 1e18)
|
||||
*/
|
||||
function setMinHealthFactor(uint256 newMinHF) external;
|
||||
|
||||
/**
|
||||
* @notice Update target health factor
|
||||
* @param newTargetHF New target health factor (scaled by 1e18)
|
||||
*/
|
||||
function setTargetHealthFactor(uint256 newTargetHF) external;
|
||||
|
||||
/**
|
||||
* @notice Add or remove allowed asset
|
||||
* @param asset Asset address
|
||||
* @param allowed Whether asset is allowed
|
||||
*/
|
||||
function setAllowedAsset(address asset, bool allowed) external;
|
||||
|
||||
/**
|
||||
* @notice Update provider capacity cap
|
||||
* @param provider Provider identifier
|
||||
* @param newCap New capacity cap
|
||||
*/
|
||||
function setProviderCap(bytes32 provider, uint256 newCap) external;
|
||||
}
|
||||
|
||||
106
contracts/interfaces/IFlashLoanRouter.sol
Normal file
106
contracts/interfaces/IFlashLoanRouter.sol
Normal file
@@ -0,0 +1,106 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/**
|
||||
* @title IFlashLoanRouter
|
||||
* @notice Interface for multi-provider flash loan router
|
||||
* @dev Aggregates flash loans from Aave, Balancer, Uniswap, DAI flash mint
|
||||
*/
|
||||
interface IFlashLoanRouter {
|
||||
enum FlashLoanProvider {
|
||||
AAVE,
|
||||
BALANCER,
|
||||
UNISWAP,
|
||||
DAI_FLASH
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Flash loan parameters
|
||||
* @param asset Asset to borrow
|
||||
* @param amount Amount to borrow
|
||||
* @param provider Provider to use (or AUTO for liquidity-weighted)
|
||||
*/
|
||||
struct FlashLoanParams {
|
||||
address asset;
|
||||
uint256 amount;
|
||||
FlashLoanProvider provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Emitted when flash loan is initiated
|
||||
* @param asset Asset borrowed
|
||||
* @param amount Amount borrowed
|
||||
* @param provider Provider used
|
||||
*/
|
||||
event FlashLoanInitiated(
|
||||
address indexed asset,
|
||||
uint256 amount,
|
||||
FlashLoanProvider provider
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when flash loan is repaid
|
||||
* @param asset Asset repaid
|
||||
* @param amount Amount repaid (principal + fee)
|
||||
*/
|
||||
event FlashLoanRepaid(address indexed asset, uint256 amount);
|
||||
|
||||
/**
|
||||
* @notice Execute a flash loan with callback
|
||||
* @param params Flash loan parameters
|
||||
* @param callbackData Data to pass to callback
|
||||
*/
|
||||
function flashLoan(
|
||||
FlashLoanParams memory params,
|
||||
bytes memory callbackData
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Execute multi-asset flash loan
|
||||
* @param params Array of flash loan parameters
|
||||
* @param callbackData Data to pass to callback
|
||||
*/
|
||||
function flashLoanBatch(
|
||||
FlashLoanParams[] memory params,
|
||||
bytes memory callbackData
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @notice Get available liquidity for an asset from a provider
|
||||
* @param asset Asset address
|
||||
* @param provider Provider to check
|
||||
* @return available Available liquidity
|
||||
*/
|
||||
function getAvailableLiquidity(
|
||||
address asset,
|
||||
FlashLoanProvider provider
|
||||
) external view returns (uint256 available);
|
||||
|
||||
/**
|
||||
* @notice Get fee for flash loan from a provider
|
||||
* @param asset Asset address
|
||||
* @param amount Amount to borrow
|
||||
* @param provider Provider to check
|
||||
* @return fee Fee amount
|
||||
*/
|
||||
function getFlashLoanFee(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
FlashLoanProvider provider
|
||||
) external view returns (uint256 fee);
|
||||
|
||||
/**
|
||||
* @notice Callback function executed during flash loan
|
||||
* @param asset Asset borrowed
|
||||
* @param amount Amount borrowed
|
||||
* @param fee Fee for the flash loan
|
||||
* @param callbackData Data passed from caller
|
||||
*/
|
||||
function onFlashLoan(
|
||||
address asset,
|
||||
uint256 amount,
|
||||
uint256 fee,
|
||||
bytes calldata callbackData
|
||||
) external returns (bytes32);
|
||||
}
|
||||
|
||||
80
contracts/interfaces/IKernel.sol
Normal file
80
contracts/interfaces/IKernel.sol
Normal file
@@ -0,0 +1,80 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./IFlashLoanRouter.sol";
|
||||
|
||||
/**
|
||||
* @title IKernel
|
||||
* @notice Interface for Recursive Leverage Kernel
|
||||
* @dev Implements atomic amortizing cycles
|
||||
*/
|
||||
interface IKernel {
|
||||
/**
|
||||
* @notice Amortization cycle parameters
|
||||
* @param targetAsset Asset to convert yield to
|
||||
* @param maxLoops Maximum number of recursive loops
|
||||
* @param minHFImprovement Minimum health factor improvement (scaled by 1e18)
|
||||
*/
|
||||
struct AmortizationParams {
|
||||
address targetAsset;
|
||||
uint256 maxLoops;
|
||||
uint256 minHFImprovement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Emitted when amortization cycle executes successfully
|
||||
* @param cyclesExecuted Number of cycles executed
|
||||
* @param collateralIncrease Increase in collateral value
|
||||
* @param debtDecrease Decrease in debt value
|
||||
* @param hfImprovement Health factor improvement
|
||||
*/
|
||||
event AmortizationExecuted(
|
||||
uint256 cyclesExecuted,
|
||||
uint256 collateralIncrease,
|
||||
uint256 debtDecrease,
|
||||
uint256 hfImprovement
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when invariant check fails
|
||||
* @param reason Reason for failure
|
||||
*/
|
||||
event InvariantFail(string reason);
|
||||
|
||||
/**
|
||||
* @notice Execute an atomic amortizing cycle
|
||||
* @param params Amortization parameters
|
||||
* @return success True if cycle succeeded
|
||||
* @return cyclesExecuted Number of cycles executed
|
||||
*/
|
||||
function executeAmortizingCycle(
|
||||
AmortizationParams memory params
|
||||
) external returns (bool success, uint256 cyclesExecuted);
|
||||
|
||||
/**
|
||||
* @notice Execute a single amortization step
|
||||
* @param flashLoanParams Flash loan parameters
|
||||
* @param targetAsset Asset to convert yield to
|
||||
* @return collateralAdded Amount of collateral added
|
||||
* @return debtRepaid Amount of debt repaid
|
||||
*/
|
||||
function executeSingleStep(
|
||||
IFlashLoanRouter.FlashLoanParams memory flashLoanParams,
|
||||
address targetAsset
|
||||
) external returns (uint256 collateralAdded, uint256 debtRepaid);
|
||||
|
||||
/**
|
||||
* @notice Verify invariants are satisfied
|
||||
* @param collateralBefore Previous collateral value
|
||||
* @param debtBefore Previous debt value
|
||||
* @param healthFactorBefore Previous health factor
|
||||
* @return success True if invariants satisfied
|
||||
* @return reason Failure reason if not successful
|
||||
*/
|
||||
function verifyInvariants(
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 healthFactorBefore
|
||||
) external view returns (bool success, string memory reason);
|
||||
}
|
||||
|
||||
70
contracts/interfaces/IOracleAdapter.sol
Normal file
70
contracts/interfaces/IOracleAdapter.sol
Normal file
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/**
|
||||
* @title IOracleAdapter
|
||||
* @notice Interface for oracle adapter
|
||||
* @dev Standardizes pricing from multiple oracle sources
|
||||
*/
|
||||
interface IOracleAdapter {
|
||||
enum OracleSource {
|
||||
AAVE,
|
||||
CHAINLINK,
|
||||
UNISWAP_TWAP,
|
||||
FALLBACK
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Price data structure
|
||||
* @param price Price (scaled by 1e8 for USD pairs)
|
||||
* @param source Oracle source used
|
||||
* @param timestamp Timestamp of price update
|
||||
* @param confidence Confidence score (0-1e18, where 1e18 = 100%)
|
||||
*/
|
||||
struct PriceData {
|
||||
uint256 price;
|
||||
OracleSource source;
|
||||
uint256 timestamp;
|
||||
uint256 confidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get latest price for an asset
|
||||
* @param asset Asset address
|
||||
* @return priceData Price data structure
|
||||
*/
|
||||
function getPrice(address asset) external view returns (PriceData memory priceData);
|
||||
|
||||
/**
|
||||
* @notice Get latest price with max age requirement
|
||||
* @param asset Asset address
|
||||
* @param maxAge Maximum age of price in seconds
|
||||
* @return priceData Price data structure
|
||||
*/
|
||||
function getPriceWithMaxAge(
|
||||
address asset,
|
||||
uint256 maxAge
|
||||
) external view returns (PriceData memory priceData);
|
||||
|
||||
/**
|
||||
* @notice Get aggregated price from multiple sources
|
||||
* @param asset Asset address
|
||||
* @return price Aggregated price (scaled by 1e8)
|
||||
* @return confidence Weighted confidence score
|
||||
*/
|
||||
function getAggregatedPrice(address asset) external view returns (uint256 price, uint256 confidence);
|
||||
|
||||
/**
|
||||
* @notice Convert amount from one asset to another using prices
|
||||
* @param fromAsset Source asset
|
||||
* @param fromAmount Amount in source asset
|
||||
* @param toAsset Destination asset
|
||||
* @return toAmount Amount in destination asset
|
||||
*/
|
||||
function convertAmount(
|
||||
address fromAsset,
|
||||
uint256 fromAmount,
|
||||
address toAsset
|
||||
) external view returns (uint256 toAmount);
|
||||
}
|
||||
|
||||
62
contracts/interfaces/IPolicyEngine.sol
Normal file
62
contracts/interfaces/IPolicyEngine.sol
Normal file
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "./IPolicyModule.sol";
|
||||
|
||||
/**
|
||||
* @title IPolicyEngine
|
||||
* @notice Interface for policy engine
|
||||
* @dev Aggregates policy decisions from multiple modules
|
||||
*/
|
||||
interface IPolicyEngine {
|
||||
/**
|
||||
* @notice Emitted when a policy module is registered
|
||||
* @param module Module address
|
||||
* @param name Module name
|
||||
*/
|
||||
event PolicyModuleRegistered(address indexed module, string name);
|
||||
|
||||
/**
|
||||
* @notice Emitted when a policy module is unregistered
|
||||
* @param module Module address
|
||||
*/
|
||||
event PolicyModuleUnregistered(address indexed module);
|
||||
|
||||
/**
|
||||
* @notice Register a policy module
|
||||
* @param module Module address
|
||||
*/
|
||||
function registerPolicyModule(address module) external;
|
||||
|
||||
/**
|
||||
* @notice Unregister a policy module
|
||||
* @param module Module address
|
||||
*/
|
||||
function unregisterPolicyModule(address module) external;
|
||||
|
||||
/**
|
||||
* @notice Evaluate all registered policy modules
|
||||
* @param actionType Type of action
|
||||
* @param actionData Action-specific data
|
||||
* @return allowed True if all modules allow the action
|
||||
* @return reason Reason for denial (if any module denies)
|
||||
*/
|
||||
function evaluateAll(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view returns (bool allowed, string memory reason);
|
||||
|
||||
/**
|
||||
* @notice Get all registered policy modules
|
||||
* @return modules Array of module addresses
|
||||
*/
|
||||
function getPolicyModules() external view returns (address[] memory modules);
|
||||
|
||||
/**
|
||||
* @notice Check if a module is registered
|
||||
* @param module Module address
|
||||
* @return registered True if registered
|
||||
*/
|
||||
function isRegistered(address module) external view returns (bool registered);
|
||||
}
|
||||
|
||||
49
contracts/interfaces/IPolicyModule.sol
Normal file
49
contracts/interfaces/IPolicyModule.sol
Normal file
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/**
|
||||
* @title IPolicyModule
|
||||
* @notice Interface for policy modules
|
||||
* @dev Policy modules enforce governance rules and risk limits
|
||||
*/
|
||||
interface IPolicyModule {
|
||||
/**
|
||||
* @notice Policy decision result
|
||||
* @param allowed Whether the action is allowed
|
||||
* @param reason Reason for denial (if not allowed)
|
||||
*/
|
||||
struct PolicyDecision {
|
||||
bool allowed;
|
||||
string reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Evaluate policy for a proposed action
|
||||
* @param actionType Type of action (encoded as bytes32)
|
||||
* @param actionData Action-specific data
|
||||
* @return decision Policy decision
|
||||
*/
|
||||
function evaluate(
|
||||
bytes32 actionType,
|
||||
bytes memory actionData
|
||||
) external view returns (PolicyDecision memory decision);
|
||||
|
||||
/**
|
||||
* @notice Get policy module name
|
||||
* @return name Module name
|
||||
*/
|
||||
function name() external pure returns (string memory name);
|
||||
|
||||
/**
|
||||
* @notice Check if module is enabled
|
||||
* @return enabled True if enabled
|
||||
*/
|
||||
function isEnabled() external view returns (bool enabled);
|
||||
|
||||
/**
|
||||
* @notice Enable or disable the module
|
||||
* @param enabled New enabled status
|
||||
*/
|
||||
function setEnabled(bool enabled) external;
|
||||
}
|
||||
|
||||
103
contracts/interfaces/IVault.sol
Normal file
103
contracts/interfaces/IVault.sol
Normal file
@@ -0,0 +1,103 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/**
|
||||
* @title IVault
|
||||
* @notice Interface for DBIS Institutional Vault
|
||||
* @dev Vault represents a leveraged position with collateral and debt tracking
|
||||
*/
|
||||
interface IVault {
|
||||
/**
|
||||
* @notice Emitted when a position snapshot is taken
|
||||
* @param collateralBefore Previous collateral amount
|
||||
* @param debtBefore Previous debt amount
|
||||
* @param collateralAfter New collateral amount
|
||||
* @param debtAfter New debt amount
|
||||
* @param healthFactorBefore Previous health factor (scaled by 1e18)
|
||||
* @param healthFactorAfter New health factor (scaled by 1e18)
|
||||
*/
|
||||
event PositionSnapshot(
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 collateralAfter,
|
||||
uint256 debtAfter,
|
||||
uint256 healthFactorBefore,
|
||||
uint256 healthFactorAfter
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Emitted when collateral is added to the position
|
||||
* @param asset Asset address
|
||||
* @param amount Amount added
|
||||
*/
|
||||
event CollateralAdded(address indexed asset, uint256 amount);
|
||||
|
||||
/**
|
||||
* @notice Emitted when debt is repaid
|
||||
* @param asset Asset address
|
||||
* @param amount Amount repaid
|
||||
*/
|
||||
event DebtRepaid(address indexed asset, uint256 amount);
|
||||
|
||||
/**
|
||||
* @notice Get total collateral value in USD (scaled by 1e8)
|
||||
*/
|
||||
function getTotalCollateralValue() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice Get total debt value in USD (scaled by 1e8)
|
||||
*/
|
||||
function getTotalDebtValue() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice Get current health factor (scaled by 1e18)
|
||||
*/
|
||||
function getHealthFactor() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice Get current LTV (Loan-to-Value ratio, scaled by 1e18)
|
||||
*/
|
||||
function getLTV() external view returns (uint256);
|
||||
|
||||
/**
|
||||
* @notice Record addition of collateral
|
||||
* @param asset Asset address
|
||||
* @param amount Amount added
|
||||
*/
|
||||
function recordCollateralAdded(address asset, uint256 amount) external;
|
||||
|
||||
/**
|
||||
* @notice Record repayment of debt
|
||||
* @param asset Asset address
|
||||
* @param amount Amount repaid
|
||||
*/
|
||||
function recordDebtRepaid(address asset, uint256 amount) external;
|
||||
|
||||
/**
|
||||
* @notice Take a position snapshot for invariant checking
|
||||
* @return collateralBefore Previous collateral value
|
||||
* @return debtBefore Previous debt value
|
||||
* @return healthFactorBefore Previous health factor
|
||||
*/
|
||||
function snapshotPosition()
|
||||
external
|
||||
returns (
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 healthFactorBefore
|
||||
);
|
||||
|
||||
/**
|
||||
* @notice Verify position improved (invariant check)
|
||||
* @param collateralBefore Previous collateral value
|
||||
* @param debtBefore Previous debt value
|
||||
* @param healthFactorBefore Previous health factor
|
||||
* @return success True if position improved
|
||||
*/
|
||||
function verifyPositionImproved(
|
||||
uint256 collateralBefore,
|
||||
uint256 debtBefore,
|
||||
uint256 healthFactorBefore
|
||||
) external view returns (bool success);
|
||||
}
|
||||
|
||||
266
contracts/oracle/DBISOracleAdapter.sol
Normal file
266
contracts/oracle/DBISOracleAdapter.sol
Normal file
@@ -0,0 +1,266 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import "../interfaces/IOracleAdapter.sol";
|
||||
|
||||
/**
|
||||
* @title DBISOracleAdapter
|
||||
* @notice Standardizes pricing from multiple oracle sources
|
||||
* @dev Aggregates prices from Aave, Chainlink, Uniswap TWAP with confidence scoring
|
||||
*/
|
||||
contract DBISOracleAdapter is IOracleAdapter, Ownable {
|
||||
// Constants
|
||||
uint256 private constant PRICE_SCALE = 1e8;
|
||||
uint256 private constant CONFIDENCE_SCALE = 1e18;
|
||||
uint256 private constant MAX_PRICE_AGE = 1 hours;
|
||||
|
||||
// Chainlink price feed interface (simplified)
|
||||
interface AggregatorV3Interface {
|
||||
function latestRoundData()
|
||||
external
|
||||
view
|
||||
returns (
|
||||
uint80 roundId,
|
||||
int256 answer,
|
||||
uint256 startedAt,
|
||||
uint256 updatedAt,
|
||||
uint80 answeredInRound
|
||||
);
|
||||
|
||||
function decimals() external view returns (uint8);
|
||||
}
|
||||
|
||||
// Aave Oracle interface (simplified)
|
||||
interface IAaveOracle {
|
||||
function getAssetPrice(address asset) external view returns (uint256);
|
||||
}
|
||||
|
||||
// Asset configuration
|
||||
struct AssetConfig {
|
||||
address chainlinkFeed;
|
||||
address aaveOracle;
|
||||
address uniswapPool;
|
||||
bool enabled;
|
||||
uint256 chainlinkWeight; // Weight for price aggregation (out of 1e18)
|
||||
uint256 aaveWeight;
|
||||
uint256 uniswapWeight;
|
||||
}
|
||||
|
||||
mapping(address => AssetConfig) public assetConfigs;
|
||||
address public immutable aaveOracleAddress;
|
||||
|
||||
// Price cache with TTL
|
||||
struct CachedPrice {
|
||||
uint256 price;
|
||||
uint256 timestamp;
|
||||
OracleSource source;
|
||||
}
|
||||
|
||||
mapping(address => CachedPrice) private priceCache;
|
||||
|
||||
event AssetConfigUpdated(address indexed asset, address chainlinkFeed, address uniswapPool);
|
||||
event PriceCacheUpdated(address indexed asset, uint256 price, OracleSource source);
|
||||
|
||||
constructor(address _aaveOracleAddress, address initialOwner) Ownable(initialOwner) {
|
||||
require(_aaveOracleAddress != address(0), "Invalid Aave Oracle");
|
||||
aaveOracleAddress = _aaveOracleAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Configure an asset's oracle sources
|
||||
*/
|
||||
function configureAsset(
|
||||
address asset,
|
||||
address chainlinkFeed,
|
||||
address uniswapPool,
|
||||
uint256 chainlinkWeight,
|
||||
uint256 aaveWeight,
|
||||
uint256 uniswapWeight
|
||||
) external onlyOwner {
|
||||
require(asset != address(0), "Invalid asset");
|
||||
require(chainlinkWeight + aaveWeight + uniswapWeight == CONFIDENCE_SCALE, "Weights must sum to 1e18");
|
||||
|
||||
assetConfigs[asset] = AssetConfig({
|
||||
chainlinkFeed: chainlinkFeed,
|
||||
aaveOracle: aaveOracleAddress,
|
||||
uniswapPool: uniswapPool,
|
||||
enabled: true,
|
||||
chainlinkWeight: chainlinkWeight,
|
||||
aaveWeight: aaveWeight,
|
||||
uniswapWeight: uniswapWeight
|
||||
});
|
||||
|
||||
emit AssetConfigUpdated(asset, chainlinkFeed, uniswapPool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Enable or disable an asset
|
||||
*/
|
||||
function setAssetEnabled(address asset, bool enabled) external onlyOwner {
|
||||
require(assetConfigs[asset].chainlinkFeed != address(0) ||
|
||||
assetConfigs[asset].aaveOracle != address(0), "Asset not configured");
|
||||
assetConfigs[asset].enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get latest price for an asset
|
||||
*/
|
||||
function getPrice(address asset) external view override returns (PriceData memory) {
|
||||
return getPriceWithMaxAge(asset, MAX_PRICE_AGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get latest price with max age requirement
|
||||
*/
|
||||
function getPriceWithMaxAge(
|
||||
address asset,
|
||||
uint256 maxAge
|
||||
) public view override returns (PriceData memory) {
|
||||
AssetConfig memory config = assetConfigs[asset];
|
||||
require(config.enabled, "Asset not enabled");
|
||||
|
||||
// Try cached price first if fresh
|
||||
CachedPrice memory cached = priceCache[asset];
|
||||
if (cached.timestamp > 0 && block.timestamp - cached.timestamp <= maxAge) {
|
||||
return PriceData({
|
||||
price: cached.price,
|
||||
source: cached.source,
|
||||
timestamp: cached.timestamp,
|
||||
confidence: CONFIDENCE_SCALE / 2 // Moderate confidence for cache
|
||||
});
|
||||
}
|
||||
|
||||
// Get prices from available sources
|
||||
uint256 chainlinkPrice = 0;
|
||||
uint256 aavePrice = 0;
|
||||
uint256 uniswapPrice = 0;
|
||||
uint256 chainlinkTime = 0;
|
||||
uint256 aaveTime = block.timestamp;
|
||||
uint256 uniswapTime = 0;
|
||||
|
||||
// Chainlink
|
||||
if (config.chainlinkFeed != address(0)) {
|
||||
try AggregatorV3Interface(config.chainlinkFeed).latestRoundData() returns (
|
||||
uint80,
|
||||
int256 answer,
|
||||
uint256,
|
||||
uint256 updatedAt,
|
||||
uint80
|
||||
) {
|
||||
if (answer > 0 && block.timestamp - updatedAt <= maxAge) {
|
||||
uint8 decimals = AggregatorV3Interface(config.chainlinkFeed).decimals();
|
||||
chainlinkPrice = uint256(answer);
|
||||
chainlinkTime = updatedAt;
|
||||
|
||||
// Normalize to 8 decimals
|
||||
if (decimals > 8) {
|
||||
chainlinkPrice = chainlinkPrice / (10 ** (decimals - 8));
|
||||
} else if (decimals < 8) {
|
||||
chainlinkPrice = chainlinkPrice * (10 ** (8 - decimals));
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Aave Oracle
|
||||
if (config.aaveOracle != address(0)) {
|
||||
try IAaveOracle(config.aaveOracle).getAssetPrice(asset) returns (uint256 price) {
|
||||
if (price > 0) {
|
||||
aavePrice = price;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Uniswap TWAP (simplified - would need actual TWAP implementation)
|
||||
// For now, return fallback
|
||||
if (config.uniswapPool != address(0)) {
|
||||
// Placeholder - would integrate with Uniswap V3 TWAP oracle
|
||||
uniswapTime = 0;
|
||||
}
|
||||
|
||||
// Aggregate prices using weights
|
||||
uint256 totalWeight = 0;
|
||||
uint256 weightedPrice = 0;
|
||||
OracleSource source = OracleSource.FALLBACK;
|
||||
uint256 latestTimestamp = 0;
|
||||
|
||||
if (chainlinkPrice > 0 && config.chainlinkWeight > 0) {
|
||||
weightedPrice += chainlinkPrice * config.chainlinkWeight;
|
||||
totalWeight += config.chainlinkWeight;
|
||||
if (chainlinkTime > latestTimestamp) {
|
||||
latestTimestamp = chainlinkTime;
|
||||
source = OracleSource.CHAINLINK;
|
||||
}
|
||||
}
|
||||
|
||||
if (aavePrice > 0 && config.aaveWeight > 0) {
|
||||
weightedPrice += aavePrice * config.aaveWeight;
|
||||
totalWeight += config.aaveWeight;
|
||||
if (aaveTime > latestTimestamp) {
|
||||
latestTimestamp = aaveTime;
|
||||
source = OracleSource.AAVE;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniswapPrice > 0 && config.uniswapWeight > 0) {
|
||||
weightedPrice += uniswapPrice * config.uniswapWeight;
|
||||
totalWeight += config.uniswapWeight;
|
||||
if (uniswapTime > latestTimestamp) {
|
||||
latestTimestamp = uniswapTime;
|
||||
source = OracleSource.UNISWAP_TWAP;
|
||||
}
|
||||
}
|
||||
|
||||
require(totalWeight > 0, "No valid price source");
|
||||
uint256 aggregatedPrice = weightedPrice / totalWeight;
|
||||
uint256 confidence = (totalWeight * CONFIDENCE_SCALE) / (config.chainlinkWeight + config.aaveWeight + config.uniswapWeight);
|
||||
|
||||
return PriceData({
|
||||
price: aggregatedPrice,
|
||||
source: source,
|
||||
timestamp: latestTimestamp > 0 ? latestTimestamp : block.timestamp,
|
||||
confidence: confidence
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Get aggregated price from multiple sources
|
||||
*/
|
||||
function getAggregatedPrice(address asset) external view override returns (uint256 price, uint256 confidence) {
|
||||
PriceData memory priceData = getPrice(asset);
|
||||
return (priceData.price, priceData.confidence);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Convert amount from one asset to another
|
||||
*/
|
||||
function convertAmount(
|
||||
address fromAsset,
|
||||
uint256 fromAmount,
|
||||
address toAsset
|
||||
) external view override returns (uint256) {
|
||||
PriceData memory fromPrice = getPrice(fromAsset);
|
||||
PriceData memory toPrice = getPrice(toAsset);
|
||||
|
||||
require(fromPrice.price > 0 && toPrice.price > 0, "Invalid prices");
|
||||
|
||||
// Both prices are in USD with 8 decimals
|
||||
// Convert: fromAmount * fromPrice / toPrice
|
||||
return (fromAmount * fromPrice.price) / toPrice.price;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Update price cache (called by keeper or internal)
|
||||
*/
|
||||
function updatePriceCache(address asset) external {
|
||||
PriceData memory priceData = getPriceWithMaxAge(asset, MAX_PRICE_AGE);
|
||||
priceCache[asset] = CachedPrice({
|
||||
price: priceData.price,
|
||||
timestamp: block.timestamp,
|
||||
source: priceData.source
|
||||
});
|
||||
emit PriceCacheUpdated(asset, priceData.price, priceData.source);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user