Add contracts/YieldRouterAgent.sol
Browse files- contracts/YieldRouterAgent.sol +263 -0
contracts/YieldRouterAgent.sol
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// SPDX-License-Identifier: MIT
|
| 2 |
+
pragma solidity ^0.8.20;
|
| 3 |
+
|
| 4 |
+
import "@openzeppelin/contracts/access/Ownable.sol";
|
| 5 |
+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
| 6 |
+
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
| 7 |
+
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* @title YieldRouterAgent
|
| 11 |
+
* @notice On-chain allocation router for the Dynamic RWA Yield Router.
|
| 12 |
+
* Records portfolio state, manages allocations, and enforces
|
| 13 |
+
* risk parameters set by the AgentIdentity8004 contract.
|
| 14 |
+
*
|
| 15 |
+
* @dev Deployed on Mantle L2 (Chain ID 5000).
|
| 16 |
+
* Works in conjunction with:
|
| 17 |
+
* - AgentIdentity8004: Agent reputation and capability attestation
|
| 18 |
+
* - RiskRegistry: On-chain risk parameters and circuit breakers
|
| 19 |
+
*
|
| 20 |
+
* The contract itself does NOT execute swaps β it records intent
|
| 21 |
+
* and validates allocations. Actual execution goes through
|
| 22 |
+
* Fluxion/Agni DEX routers via ERC-4337 wallet.
|
| 23 |
+
*/
|
| 24 |
+
contract YieldRouterAgent is Ownable, ReentrancyGuard {
|
| 25 |
+
using SafeERC20 for IERC20;
|
| 26 |
+
|
| 27 |
+
// βββββββββββββββββββ State βββββββββββββββββββββββ
|
| 28 |
+
|
| 29 |
+
/// @notice Supported RWA assets
|
| 30 |
+
struct Asset {
|
| 31 |
+
address token;
|
| 32 |
+
string symbol;
|
| 33 |
+
uint256 targetWeight; // basis points (10000 = 100%)
|
| 34 |
+
uint256 currentWeight; // basis points
|
| 35 |
+
uint256 lastPrice; // USD price * 1e18
|
| 36 |
+
bool active;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/// @notice Allocation record (on-chain audit trail)
|
| 40 |
+
struct AllocationRecord {
|
| 41 |
+
uint256 timestamp;
|
| 42 |
+
uint256[3] weights; // [USDY, mETH, MI4] in bps
|
| 43 |
+
uint256 portfolioValue; // USD * 1e18
|
| 44 |
+
bytes32 reasonHash; // IPFS/content hash of strategy report
|
| 45 |
+
address authorizedBy; // EOA or ERC-4337 wallet that approved
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/// @notice Circuit breaker states
|
| 49 |
+
enum CircuitBreakerState { CLOSED, OPEN, HALF_OPEN }
|
| 50 |
+
|
| 51 |
+
// Assets
|
| 52 |
+
Asset[3] public assets;
|
| 53 |
+
|
| 54 |
+
// Allocation history
|
| 55 |
+
AllocationRecord[] public allocationHistory;
|
| 56 |
+
|
| 57 |
+
// Agent identity
|
| 58 |
+
address public agentIdentityContract;
|
| 59 |
+
address public riskRegistryContract;
|
| 60 |
+
|
| 61 |
+
// Portfolio
|
| 62 |
+
uint256 public totalPortfolioValue; // USD * 1e18
|
| 63 |
+
uint256 public lastRebalanceTime;
|
| 64 |
+
uint256 public totalRebalances;
|
| 65 |
+
|
| 66 |
+
// Risk parameters
|
| 67 |
+
uint256 public maxSingleAssetWeight = 6000; // 60% in bps
|
| 68 |
+
uint256 public minSingleAssetWeight = 500; // 5% in bps
|
| 69 |
+
uint256 public minRebalanceInterval = 4 hours;
|
| 70 |
+
uint256 public maxSlippageBps = 50; // 0.5%
|
| 71 |
+
CircuitBreakerState public circuitBreaker = CircuitBreakerState.CLOSED;
|
| 72 |
+
|
| 73 |
+
// Authorized operators (ERC-4337 wallet, keeper bots)
|
| 74 |
+
mapping(address => bool) public authorizedOperators;
|
| 75 |
+
|
| 76 |
+
// βββββββββββββββββββ Events βββββββββββββββββββββββ
|
| 77 |
+
|
| 78 |
+
event AllocationUpdated(
|
| 79 |
+
uint256 indexed recordId,
|
| 80 |
+
uint256[3] weights,
|
| 81 |
+
uint256 portfolioValue,
|
| 82 |
+
bytes32 reasonHash,
|
| 83 |
+
address indexed authorizedBy
|
| 84 |
+
);
|
| 85 |
+
|
| 86 |
+
event AssetUpdated(uint256 indexed assetIndex, address token, string symbol, bool active);
|
| 87 |
+
event CircuitBreakerTriggered(CircuitBreakerState newState, string reason);
|
| 88 |
+
event OperatorAuthorized(address indexed operator, bool authorized);
|
| 89 |
+
event RiskParametersUpdated(uint256 maxWeight, uint256 minWeight, uint256 minInterval);
|
| 90 |
+
|
| 91 |
+
// βββββββββββββββββββ Modifiers βββββββββββββββββββ
|
| 92 |
+
|
| 93 |
+
modifier onlyAuthorized() {
|
| 94 |
+
require(
|
| 95 |
+
msg.sender == owner() || authorizedOperators[msg.sender],
|
| 96 |
+
"Not authorized"
|
| 97 |
+
);
|
| 98 |
+
_;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
modifier circuitBreakerClosed() {
|
| 102 |
+
require(
|
| 103 |
+
circuitBreaker != CircuitBreakerState.OPEN,
|
| 104 |
+
"Circuit breaker is OPEN"
|
| 105 |
+
);
|
| 106 |
+
_;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// βββββββββββββββββββ Constructor βββββββββββββββββ
|
| 110 |
+
|
| 111 |
+
constructor(
|
| 112 |
+
address _usdy,
|
| 113 |
+
address _meth,
|
| 114 |
+
address _mi4,
|
| 115 |
+
address _agentIdentity,
|
| 116 |
+
address _riskRegistry
|
| 117 |
+
) Ownable(msg.sender) {
|
| 118 |
+
assets[0] = Asset(_usdy, "USDY", 4000, 4000, 1e18, true);
|
| 119 |
+
assets[1] = Asset(_meth, "mETH", 3500, 3500, 3200e18, true);
|
| 120 |
+
assets[2] = Asset(_mi4, "MI4", 2500, 2500, 100e18, true);
|
| 121 |
+
|
| 122 |
+
agentIdentityContract = _agentIdentity;
|
| 123 |
+
riskRegistryContract = _riskRegistry;
|
| 124 |
+
lastRebalanceTime = block.timestamp;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// βββββββββββββββββββ Core Functions ββββββββββββββ
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* @notice Record a new allocation decision on-chain.
|
| 131 |
+
* @param weights Target weights in basis points [USDY, mETH, MI4]
|
| 132 |
+
* @param portfolioValue Portfolio value in USD * 1e18
|
| 133 |
+
* @param reasonHash Content hash of strategy report (IPFS CID or SHA-256)
|
| 134 |
+
*/
|
| 135 |
+
function recordAllocation(
|
| 136 |
+
uint256[3] calldata weights,
|
| 137 |
+
uint256 portfolioValue,
|
| 138 |
+
bytes32 reasonHash
|
| 139 |
+
) external onlyAuthorized circuitBreakerClosed nonReentrant {
|
| 140 |
+
// Validate weights sum to 10000 bps
|
| 141 |
+
require(
|
| 142 |
+
weights[0] + weights[1] + weights[2] == 10000,
|
| 143 |
+
"Weights must sum to 10000 bps"
|
| 144 |
+
);
|
| 145 |
+
|
| 146 |
+
// Validate position limits
|
| 147 |
+
for (uint256 i = 0; i < 3; i++) {
|
| 148 |
+
require(
|
| 149 |
+
weights[i] >= minSingleAssetWeight,
|
| 150 |
+
"Weight below minimum"
|
| 151 |
+
);
|
| 152 |
+
require(
|
| 153 |
+
weights[i] <= maxSingleAssetWeight,
|
| 154 |
+
"Weight above maximum"
|
| 155 |
+
);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
// Validate rebalance interval
|
| 159 |
+
require(
|
| 160 |
+
block.timestamp >= lastRebalanceTime + minRebalanceInterval,
|
| 161 |
+
"Rebalance too soon"
|
| 162 |
+
);
|
| 163 |
+
|
| 164 |
+
// Record allocation
|
| 165 |
+
uint256 recordId = allocationHistory.length;
|
| 166 |
+
allocationHistory.push(AllocationRecord({
|
| 167 |
+
timestamp: block.timestamp,
|
| 168 |
+
weights: weights,
|
| 169 |
+
portfolioValue: portfolioValue,
|
| 170 |
+
reasonHash: reasonHash,
|
| 171 |
+
authorizedBy: msg.sender
|
| 172 |
+
}));
|
| 173 |
+
|
| 174 |
+
// Update state
|
| 175 |
+
for (uint256 i = 0; i < 3; i++) {
|
| 176 |
+
assets[i].targetWeight = weights[i];
|
| 177 |
+
assets[i].currentWeight = weights[i];
|
| 178 |
+
}
|
| 179 |
+
totalPortfolioValue = portfolioValue;
|
| 180 |
+
lastRebalanceTime = block.timestamp;
|
| 181 |
+
totalRebalances++;
|
| 182 |
+
|
| 183 |
+
emit AllocationUpdated(recordId, weights, portfolioValue, reasonHash, msg.sender);
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/**
|
| 187 |
+
* @notice Trigger circuit breaker (emergency stop).
|
| 188 |
+
* @param reason Human-readable reason for the halt
|
| 189 |
+
*/
|
| 190 |
+
function triggerCircuitBreaker(string calldata reason) external onlyAuthorized {
|
| 191 |
+
circuitBreaker = CircuitBreakerState.OPEN;
|
| 192 |
+
emit CircuitBreakerTriggered(CircuitBreakerState.OPEN, reason);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/**
|
| 196 |
+
* @notice Reset circuit breaker to closed state.
|
| 197 |
+
*/
|
| 198 |
+
function resetCircuitBreaker() external onlyOwner {
|
| 199 |
+
circuitBreaker = CircuitBreakerState.CLOSED;
|
| 200 |
+
emit CircuitBreakerTriggered(CircuitBreakerState.CLOSED, "Manual reset");
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
// βββββββββββββββββββ Admin Functions βββββββββββββ
|
| 204 |
+
|
| 205 |
+
function setOperator(address operator, bool authorized) external onlyOwner {
|
| 206 |
+
authorizedOperators[operator] = authorized;
|
| 207 |
+
emit OperatorAuthorized(operator, authorized);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
function setRiskParameters(
|
| 211 |
+
uint256 _maxWeight,
|
| 212 |
+
uint256 _minWeight,
|
| 213 |
+
uint256 _minInterval,
|
| 214 |
+
uint256 _maxSlippage
|
| 215 |
+
) external onlyOwner {
|
| 216 |
+
require(_maxWeight <= 9000, "Max weight too high");
|
| 217 |
+
require(_minWeight >= 100, "Min weight too low");
|
| 218 |
+
maxSingleAssetWeight = _maxWeight;
|
| 219 |
+
minSingleAssetWeight = _minWeight;
|
| 220 |
+
minRebalanceInterval = _minInterval;
|
| 221 |
+
maxSlippageBps = _maxSlippage;
|
| 222 |
+
emit RiskParametersUpdated(_maxWeight, _minWeight, _minInterval);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
function updateAsset(
|
| 226 |
+
uint256 index,
|
| 227 |
+
address token,
|
| 228 |
+
string calldata symbol,
|
| 229 |
+
bool active
|
| 230 |
+
) external onlyOwner {
|
| 231 |
+
require(index < 3, "Invalid asset index");
|
| 232 |
+
assets[index].token = token;
|
| 233 |
+
assets[index].symbol = symbol;
|
| 234 |
+
assets[index].active = active;
|
| 235 |
+
emit AssetUpdated(index, token, symbol, active);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// βββββββββββββββββββ View Functions ββββββββββββββ
|
| 239 |
+
|
| 240 |
+
function getCurrentWeights() external view returns (uint256[3] memory) {
|
| 241 |
+
return [
|
| 242 |
+
assets[0].currentWeight,
|
| 243 |
+
assets[1].currentWeight,
|
| 244 |
+
assets[2].currentWeight
|
| 245 |
+
];
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function getAllocationCount() external view returns (uint256) {
|
| 249 |
+
return allocationHistory.length;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
function getLatestAllocation() external view returns (AllocationRecord memory) {
|
| 253 |
+
require(allocationHistory.length > 0, "No allocations yet");
|
| 254 |
+
return allocationHistory[allocationHistory.length - 1];
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
/**
|
| 258 |
+
* @notice Emergency withdraw tokens (owner only, for contract migration).
|
| 259 |
+
*/
|
| 260 |
+
function emergencyWithdraw(address token, uint256 amount) external onlyOwner {
|
| 261 |
+
IERC20(token).safeTransfer(owner(), amount);
|
| 262 |
+
}
|
| 263 |
+
}
|