mantle-rwa-yield-router / contracts /YieldRouterAgent.sol
muthuk1's picture
Add contracts/YieldRouterAgent.sol
97b66f6 verified
// 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);
}
}