| |
| pragma solidity ^0.8.20; |
|
|
| import "@openzeppelin/contracts/access/Ownable.sol"; |
| import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; |
| import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; |
| import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| contract YieldRouterAgent is Ownable, ReentrancyGuard { |
| using SafeERC20 for IERC20; |
|
|
| |
|
|
| |
| struct Asset { |
| address token; |
| string symbol; |
| uint256 targetWeight; |
| uint256 currentWeight; |
| uint256 lastPrice; |
| bool active; |
| } |
|
|
| |
| struct AllocationRecord { |
| uint256 timestamp; |
| uint256[3] weights; |
| uint256 portfolioValue; |
| bytes32 reasonHash; |
| address authorizedBy; |
| } |
|
|
| |
| enum CircuitBreakerState { CLOSED, OPEN, HALF_OPEN } |
|
|
| |
| Asset[3] public assets; |
| |
| |
| AllocationRecord[] public allocationHistory; |
| |
| |
| address public agentIdentityContract; |
| address public riskRegistryContract; |
| |
| |
| uint256 public totalPortfolioValue; |
| uint256 public lastRebalanceTime; |
| uint256 public totalRebalances; |
| |
| |
| uint256 public maxSingleAssetWeight = 6000; |
| uint256 public minSingleAssetWeight = 500; |
| uint256 public minRebalanceInterval = 4 hours; |
| uint256 public maxSlippageBps = 50; |
| CircuitBreakerState public circuitBreaker = CircuitBreakerState.CLOSED; |
| |
| |
| mapping(address => bool) public authorizedOperators; |
|
|
| |
|
|
| event AllocationUpdated( |
| uint256 indexed recordId, |
| uint256[3] weights, |
| uint256 portfolioValue, |
| bytes32 reasonHash, |
| address indexed authorizedBy |
| ); |
| |
| event AssetUpdated(uint256 indexed assetIndex, address token, string symbol, bool active); |
| event CircuitBreakerTriggered(CircuitBreakerState newState, string reason); |
| event OperatorAuthorized(address indexed operator, bool authorized); |
| event RiskParametersUpdated(uint256 maxWeight, uint256 minWeight, uint256 minInterval); |
|
|
| |
|
|
| modifier onlyAuthorized() { |
| require( |
| msg.sender == owner() || authorizedOperators[msg.sender], |
| "Not authorized" |
| ); |
| _; |
| } |
|
|
| modifier circuitBreakerClosed() { |
| require( |
| circuitBreaker != CircuitBreakerState.OPEN, |
| "Circuit breaker is OPEN" |
| ); |
| _; |
| } |
|
|
| |
|
|
| constructor( |
| address _usdy, |
| address _meth, |
| address _mi4, |
| address _agentIdentity, |
| address _riskRegistry |
| ) Ownable(msg.sender) { |
| assets[0] = Asset(_usdy, "USDY", 4000, 4000, 1e18, true); |
| assets[1] = Asset(_meth, "mETH", 3500, 3500, 3200e18, true); |
| assets[2] = Asset(_mi4, "MI4", 2500, 2500, 100e18, true); |
| |
| agentIdentityContract = _agentIdentity; |
| riskRegistryContract = _riskRegistry; |
| lastRebalanceTime = block.timestamp; |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| function recordAllocation( |
| uint256[3] calldata weights, |
| uint256 portfolioValue, |
| bytes32 reasonHash |
| ) external onlyAuthorized circuitBreakerClosed nonReentrant { |
| |
| require( |
| weights[0] + weights[1] + weights[2] == 10000, |
| "Weights must sum to 10000 bps" |
| ); |
| |
| |
| for (uint256 i = 0; i < 3; i++) { |
| require( |
| weights[i] >= minSingleAssetWeight, |
| "Weight below minimum" |
| ); |
| require( |
| weights[i] <= maxSingleAssetWeight, |
| "Weight above maximum" |
| ); |
| } |
| |
| |
| require( |
| block.timestamp >= lastRebalanceTime + minRebalanceInterval, |
| "Rebalance too soon" |
| ); |
| |
| |
| uint256 recordId = allocationHistory.length; |
| allocationHistory.push(AllocationRecord({ |
| timestamp: block.timestamp, |
| weights: weights, |
| portfolioValue: portfolioValue, |
| reasonHash: reasonHash, |
| authorizedBy: msg.sender |
| })); |
| |
| |
| for (uint256 i = 0; i < 3; i++) { |
| assets[i].targetWeight = weights[i]; |
| assets[i].currentWeight = weights[i]; |
| } |
| totalPortfolioValue = portfolioValue; |
| lastRebalanceTime = block.timestamp; |
| totalRebalances++; |
| |
| emit AllocationUpdated(recordId, weights, portfolioValue, reasonHash, msg.sender); |
| } |
|
|
| |
| |
| |
| |
| function triggerCircuitBreaker(string calldata reason) external onlyAuthorized { |
| circuitBreaker = CircuitBreakerState.OPEN; |
| emit CircuitBreakerTriggered(CircuitBreakerState.OPEN, reason); |
| } |
|
|
| |
| |
| |
| function resetCircuitBreaker() external onlyOwner { |
| circuitBreaker = CircuitBreakerState.CLOSED; |
| emit CircuitBreakerTriggered(CircuitBreakerState.CLOSED, "Manual reset"); |
| } |
|
|
| |
|
|
| function setOperator(address operator, bool authorized) external onlyOwner { |
| authorizedOperators[operator] = authorized; |
| emit OperatorAuthorized(operator, authorized); |
| } |
|
|
| function setRiskParameters( |
| uint256 _maxWeight, |
| uint256 _minWeight, |
| uint256 _minInterval, |
| uint256 _maxSlippage |
| ) external onlyOwner { |
| require(_maxWeight <= 9000, "Max weight too high"); |
| require(_minWeight >= 100, "Min weight too low"); |
| maxSingleAssetWeight = _maxWeight; |
| minSingleAssetWeight = _minWeight; |
| minRebalanceInterval = _minInterval; |
| maxSlippageBps = _maxSlippage; |
| emit RiskParametersUpdated(_maxWeight, _minWeight, _minInterval); |
| } |
|
|
| function updateAsset( |
| uint256 index, |
| address token, |
| string calldata symbol, |
| bool active |
| ) external onlyOwner { |
| require(index < 3, "Invalid asset index"); |
| assets[index].token = token; |
| assets[index].symbol = symbol; |
| assets[index].active = active; |
| emit AssetUpdated(index, token, symbol, active); |
| } |
|
|
| |
|
|
| function getCurrentWeights() external view returns (uint256[3] memory) { |
| return [ |
| assets[0].currentWeight, |
| assets[1].currentWeight, |
| assets[2].currentWeight |
| ]; |
| } |
|
|
| function getAllocationCount() external view returns (uint256) { |
| return allocationHistory.length; |
| } |
|
|
| function getLatestAllocation() external view returns (AllocationRecord memory) { |
| require(allocationHistory.length > 0, "No allocations yet"); |
| return allocationHistory[allocationHistory.length - 1]; |
| } |
|
|
| |
| |
| |
| function emergencyWithdraw(address token, uint256 amount) external onlyOwner { |
| IERC20(token).safeTransfer(owner(), amount); |
| } |
| } |
|
|