// SPDX-License-Identifier: MIT 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"; /** * @title YieldRouterAgent * @notice On-chain allocation router for the Dynamic RWA Yield Router. * Records portfolio state, manages allocations, and enforces * risk parameters set by the AgentIdentity8004 contract. * * @dev Deployed on Mantle L2 (Chain ID 5000). * Works in conjunction with: * - AgentIdentity8004: Agent reputation and capability attestation * - RiskRegistry: On-chain risk parameters and circuit breakers * * The contract itself does NOT execute swaps — it records intent * and validates allocations. Actual execution goes through * Fluxion/Agni DEX routers via ERC-4337 wallet. */ contract YieldRouterAgent is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; // ─────────────────── State ─────────────────────── /// @notice Supported RWA assets struct Asset { address token; string symbol; uint256 targetWeight; // basis points (10000 = 100%) uint256 currentWeight; // basis points uint256 lastPrice; // USD price * 1e18 bool active; } /// @notice Allocation record (on-chain audit trail) struct AllocationRecord { uint256 timestamp; uint256[3] weights; // [USDY, mETH, MI4] in bps uint256 portfolioValue; // USD * 1e18 bytes32 reasonHash; // IPFS/content hash of strategy report address authorizedBy; // EOA or ERC-4337 wallet that approved } /// @notice Circuit breaker states enum CircuitBreakerState { CLOSED, OPEN, HALF_OPEN } // Assets Asset[3] public assets; // Allocation history AllocationRecord[] public allocationHistory; // Agent identity address public agentIdentityContract; address public riskRegistryContract; // Portfolio uint256 public totalPortfolioValue; // USD * 1e18 uint256 public lastRebalanceTime; uint256 public totalRebalances; // Risk parameters uint256 public maxSingleAssetWeight = 6000; // 60% in bps uint256 public minSingleAssetWeight = 500; // 5% in bps uint256 public minRebalanceInterval = 4 hours; uint256 public maxSlippageBps = 50; // 0.5% CircuitBreakerState public circuitBreaker = CircuitBreakerState.CLOSED; // Authorized operators (ERC-4337 wallet, keeper bots) mapping(address => bool) public authorizedOperators; // ─────────────────── Events ─────────────────────── 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); // ─────────────────── Modifiers ─────────────────── modifier onlyAuthorized() { require( msg.sender == owner() || authorizedOperators[msg.sender], "Not authorized" ); _; } modifier circuitBreakerClosed() { require( circuitBreaker != CircuitBreakerState.OPEN, "Circuit breaker is OPEN" ); _; } // ─────────────────── Constructor ───────────────── 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; } // ─────────────────── Core Functions ────────────── /** * @notice Record a new allocation decision on-chain. * @param weights Target weights in basis points [USDY, mETH, MI4] * @param portfolioValue Portfolio value in USD * 1e18 * @param reasonHash Content hash of strategy report (IPFS CID or SHA-256) */ function recordAllocation( uint256[3] calldata weights, uint256 portfolioValue, bytes32 reasonHash ) external onlyAuthorized circuitBreakerClosed nonReentrant { // Validate weights sum to 10000 bps require( weights[0] + weights[1] + weights[2] == 10000, "Weights must sum to 10000 bps" ); // Validate position limits for (uint256 i = 0; i < 3; i++) { require( weights[i] >= minSingleAssetWeight, "Weight below minimum" ); require( weights[i] <= maxSingleAssetWeight, "Weight above maximum" ); } // Validate rebalance interval require( block.timestamp >= lastRebalanceTime + minRebalanceInterval, "Rebalance too soon" ); // Record allocation uint256 recordId = allocationHistory.length; allocationHistory.push(AllocationRecord({ timestamp: block.timestamp, weights: weights, portfolioValue: portfolioValue, reasonHash: reasonHash, authorizedBy: msg.sender })); // Update state 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); } /** * @notice Trigger circuit breaker (emergency stop). * @param reason Human-readable reason for the halt */ function triggerCircuitBreaker(string calldata reason) external onlyAuthorized { circuitBreaker = CircuitBreakerState.OPEN; emit CircuitBreakerTriggered(CircuitBreakerState.OPEN, reason); } /** * @notice Reset circuit breaker to closed state. */ function resetCircuitBreaker() external onlyOwner { circuitBreaker = CircuitBreakerState.CLOSED; emit CircuitBreakerTriggered(CircuitBreakerState.CLOSED, "Manual reset"); } // ─────────────────── Admin Functions ───────────── 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); } // ─────────────────── View Functions ────────────── 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]; } /** * @notice Emergency withdraw tokens (owner only, for contract migration). */ function emergencyWithdraw(address token, uint256 amount) external onlyOwner { IERC20(token).safeTransfer(owner(), amount); } }