- Introduced Aggregator.sol for Chainlink-compatible oracle functionality, including round-based updates and access control. - Added OracleWithCCIP.sol to extend Aggregator with CCIP cross-chain messaging capabilities. - Created .gitmodules to include OpenZeppelin contracts as a submodule. - Developed a comprehensive deployment guide in NEXT_STEPS_COMPLETE_GUIDE.md for Phase 2 and smart contract deployment. - Implemented Vite configuration for the orchestration portal, supporting both Vue and React frameworks. - Added server-side logic for the Multi-Cloud Orchestration Portal, including API endpoints for environment management and monitoring. - Created scripts for resource import and usage validation across non-US regions. - Added tests for CCIP error handling and integration to ensure robust functionality. - Included various new files and directories for the orchestration portal and deployment scripts.
327 lines
12 KiB
Solidity
327 lines
12 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity ^0.8.20;
|
|
|
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
|
import "./IReserveSystem.sol";
|
|
|
|
/**
|
|
* @title ReserveSystem
|
|
* @notice Core implementation of the GRU Reserve System
|
|
* @dev Manages reserves in multiple asset classes (XAU, digital assets, sovereign instruments)
|
|
* Implements XAU triangulation conversion algorithm and redemption mechanisms
|
|
*/
|
|
contract ReserveSystem is IReserveSystem, AccessControl, ReentrancyGuard {
|
|
using SafeERC20 for IERC20;
|
|
|
|
// ============ Constants ============
|
|
|
|
bytes32 public constant RESERVE_MANAGER_ROLE = keccak256("RESERVE_MANAGER_ROLE");
|
|
bytes32 public constant PRICE_FEED_ROLE = keccak256("PRICE_FEED_ROLE");
|
|
bytes32 public constant CONVERSION_OPERATOR_ROLE = keccak256("CONVERSION_OPERATOR_ROLE");
|
|
|
|
// Conversion fee parameters (basis points: 10000 = 100%)
|
|
uint256 public constant BASE_FEE_BPS = 10; // 0.1%
|
|
uint256 public constant SLIPPAGE_FEE_BPS = 50; // 0.5% per 1% slippage
|
|
uint256 public constant LARGE_TRANSACTION_THRESHOLD = 1_000_000 * 1e18; // $1M
|
|
uint256 public constant LARGE_TRANSACTION_FEE_BPS = 5; // 0.05%
|
|
|
|
// Price staleness threshold (30 seconds)
|
|
uint256 public constant PRICE_STALENESS_THRESHOLD = 30;
|
|
|
|
// Maximum slippage (0.5% for liquid, 1.0% for less liquid)
|
|
uint256 public constant MAX_SLIPPAGE_BPS = 50; // 0.5%
|
|
|
|
// ============ Storage ============
|
|
|
|
struct Reserve {
|
|
address asset;
|
|
uint256 balance;
|
|
uint256 lastUpdated;
|
|
}
|
|
|
|
struct PriceFeed {
|
|
uint256 price;
|
|
uint256 timestamp;
|
|
bool isValid;
|
|
}
|
|
|
|
struct Conversion {
|
|
address sourceAsset;
|
|
address targetAsset;
|
|
uint256 sourceAmount;
|
|
uint256 targetAmount;
|
|
uint256 fees;
|
|
address[] path;
|
|
uint256 timestamp;
|
|
}
|
|
|
|
// Reserve tracking
|
|
mapping(address => uint256) public reserveBalances;
|
|
mapping(bytes32 => Reserve) public reserves;
|
|
bytes32[] public reserveIds;
|
|
|
|
// Price feeds
|
|
mapping(address => PriceFeed) public priceFeeds;
|
|
address[] public supportedAssets;
|
|
|
|
// Conversion tracking
|
|
mapping(bytes32 => Conversion) public conversions;
|
|
bytes32[] public conversionIds;
|
|
|
|
// Asset metadata
|
|
mapping(address => bool) public isSupportedAsset;
|
|
mapping(address => bool) public isLiquidAsset; // For slippage calculation
|
|
|
|
// ============ Constructor ============
|
|
|
|
constructor(address admin) {
|
|
_grantRole(DEFAULT_ADMIN_ROLE, admin);
|
|
_grantRole(RESERVE_MANAGER_ROLE, admin);
|
|
_grantRole(PRICE_FEED_ROLE, admin);
|
|
_grantRole(CONVERSION_OPERATOR_ROLE, admin);
|
|
}
|
|
|
|
// ============ Reserve Management ============
|
|
|
|
function depositReserve(
|
|
address asset,
|
|
uint256 amount
|
|
) external override onlyRole(RESERVE_MANAGER_ROLE) nonReentrant returns (bytes32 reserveId) {
|
|
require(asset != address(0), "ReserveSystem: zero address");
|
|
require(amount > 0, "ReserveSystem: zero amount");
|
|
require(isSupportedAsset[asset], "ReserveSystem: unsupported asset");
|
|
|
|
IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
|
|
|
|
reserveId = keccak256(abi.encodePacked(asset, amount, block.timestamp, reserveIds.length));
|
|
reserves[reserveId] = Reserve({
|
|
asset: asset,
|
|
balance: amount,
|
|
lastUpdated: block.timestamp
|
|
});
|
|
reserveIds.push(reserveId);
|
|
reserveBalances[asset] += amount;
|
|
|
|
emit ReserveDeposited(asset, amount, msg.sender, reserveId);
|
|
}
|
|
|
|
function withdrawReserve(
|
|
address asset,
|
|
uint256 amount,
|
|
address recipient
|
|
) external override onlyRole(RESERVE_MANAGER_ROLE) nonReentrant returns (bytes32 reserveId) {
|
|
require(asset != address(0), "ReserveSystem: zero address");
|
|
require(recipient != address(0), "ReserveSystem: zero recipient");
|
|
require(amount > 0, "ReserveSystem: zero amount");
|
|
require(reserveBalances[asset] >= amount, "ReserveSystem: insufficient reserve");
|
|
|
|
reserveBalances[asset] -= amount;
|
|
|
|
IERC20(asset).safeTransfer(recipient, amount);
|
|
|
|
reserveId = keccak256(abi.encodePacked(asset, amount, block.timestamp, reserveIds.length));
|
|
|
|
emit ReserveWithdrawn(asset, amount, recipient, reserveId);
|
|
}
|
|
|
|
function getReserveBalance(address asset) external view override returns (uint256) {
|
|
return reserveBalances[asset];
|
|
}
|
|
|
|
function getReserveById(bytes32 reserveId) external view override returns (address asset, uint256 balance) {
|
|
Reserve memory reserve = reserves[reserveId];
|
|
return (reserve.asset, reserve.balance);
|
|
}
|
|
|
|
// ============ Conversion ============
|
|
|
|
function convertAssets(
|
|
address sourceAsset,
|
|
address targetAsset,
|
|
uint256 amount
|
|
) external override onlyRole(CONVERSION_OPERATOR_ROLE) nonReentrant returns (
|
|
bytes32 conversionId,
|
|
uint256 targetAmount,
|
|
uint256 fees
|
|
) {
|
|
require(sourceAsset != address(0), "ReserveSystem: zero source asset");
|
|
require(targetAsset != address(0), "ReserveSystem: zero target asset");
|
|
require(amount > 0, "ReserveSystem: zero amount");
|
|
require(isSupportedAsset[sourceAsset], "ReserveSystem: unsupported source asset");
|
|
require(isSupportedAsset[targetAsset], "ReserveSystem: unsupported target asset");
|
|
|
|
// Calculate conversion
|
|
(uint256 calculatedAmount, uint256 calculatedFees, address[] memory path) =
|
|
calculateConversion(sourceAsset, targetAsset, amount);
|
|
|
|
require(reserveBalances[targetAsset] >= calculatedAmount, "ReserveSystem: insufficient target reserve");
|
|
|
|
// Transfer source asset
|
|
IERC20(sourceAsset).safeTransferFrom(msg.sender, address(this), amount);
|
|
reserveBalances[sourceAsset] += amount;
|
|
|
|
// Transfer target asset
|
|
reserveBalances[targetAsset] -= calculatedAmount;
|
|
IERC20(targetAsset).safeTransfer(msg.sender, calculatedAmount);
|
|
|
|
conversionId = keccak256(abi.encodePacked(sourceAsset, targetAsset, amount, block.timestamp, conversionIds.length));
|
|
conversions[conversionId] = Conversion({
|
|
sourceAsset: sourceAsset,
|
|
targetAsset: targetAsset,
|
|
sourceAmount: amount,
|
|
targetAmount: calculatedAmount,
|
|
fees: calculatedFees,
|
|
path: path,
|
|
timestamp: block.timestamp
|
|
});
|
|
conversionIds.push(conversionId);
|
|
|
|
emit ConversionExecuted(sourceAsset, targetAsset, amount, calculatedAmount, conversionId, calculatedFees);
|
|
|
|
return (conversionId, calculatedAmount, calculatedFees);
|
|
}
|
|
|
|
function calculateConversion(
|
|
address sourceAsset,
|
|
address targetAsset,
|
|
uint256 amount
|
|
) public view override returns (
|
|
uint256 targetAmount,
|
|
uint256 fees,
|
|
address[] memory path
|
|
) {
|
|
require(sourceAsset != address(0), "ReserveSystem: zero source asset");
|
|
require(targetAsset != address(0), "ReserveSystem: zero target asset");
|
|
require(amount > 0, "ReserveSystem: zero amount");
|
|
|
|
// Get prices
|
|
(uint256 sourcePrice, uint256 sourceTimestamp) = getPrice(sourceAsset);
|
|
(uint256 targetPrice, uint256 targetTimestamp) = getPrice(targetAsset);
|
|
|
|
require(sourceTimestamp > 0 && targetTimestamp > 0, "ReserveSystem: price feed not available");
|
|
require(block.timestamp - sourceTimestamp <= PRICE_STALENESS_THRESHOLD, "ReserveSystem: stale source price");
|
|
require(block.timestamp - targetTimestamp <= PRICE_STALENESS_THRESHOLD, "ReserveSystem: stale target price");
|
|
|
|
// Direct conversion
|
|
targetAmount = (amount * targetPrice) / sourcePrice;
|
|
|
|
// Calculate fees
|
|
fees = calculateFees(targetAmount, 0); // No slippage for direct conversion
|
|
|
|
// Simple path (direct conversion)
|
|
path = new address[](2);
|
|
path[0] = sourceAsset;
|
|
path[1] = targetAsset;
|
|
|
|
return (targetAmount, fees, path);
|
|
}
|
|
|
|
function calculateFees(uint256 amount, uint256 slippageBps) internal pure returns (uint256) {
|
|
uint256 baseFee = (amount * BASE_FEE_BPS) / 10000;
|
|
uint256 slippageFee = 0;
|
|
|
|
if (slippageBps > 10) { // 0.1% threshold
|
|
slippageFee = (amount * slippageBps * SLIPPAGE_FEE_BPS) / 1000000;
|
|
}
|
|
|
|
uint256 largeTransactionFee = 0;
|
|
if (amount >= LARGE_TRANSACTION_THRESHOLD) {
|
|
largeTransactionFee = (amount * LARGE_TRANSACTION_FEE_BPS) / 10000;
|
|
}
|
|
|
|
return baseFee + slippageFee + largeTransactionFee;
|
|
}
|
|
|
|
// ============ Redemption ============
|
|
|
|
function redeem(
|
|
address asset,
|
|
uint256 amount,
|
|
address recipient
|
|
) external override onlyRole(RESERVE_MANAGER_ROLE) nonReentrant returns (bytes32 redemptionId) {
|
|
require(asset != address(0), "ReserveSystem: zero address");
|
|
require(recipient != address(0), "ReserveSystem: zero recipient");
|
|
require(amount > 0, "ReserveSystem: zero amount");
|
|
require(reserveBalances[asset] >= amount, "ReserveSystem: insufficient reserve");
|
|
|
|
reserveBalances[asset] -= amount;
|
|
IERC20(asset).safeTransfer(recipient, amount);
|
|
|
|
redemptionId = keccak256(abi.encodePacked(asset, amount, block.timestamp, conversionIds.length));
|
|
|
|
emit RedemptionExecuted(asset, amount, recipient, redemptionId);
|
|
}
|
|
|
|
// ============ Price Feeds ============
|
|
|
|
function updatePriceFeed(
|
|
address asset,
|
|
uint256 price,
|
|
uint256 timestamp
|
|
) external override onlyRole(PRICE_FEED_ROLE) {
|
|
require(asset != address(0), "ReserveSystem: zero address");
|
|
require(price > 0, "ReserveSystem: zero price");
|
|
require(timestamp <= block.timestamp, "ReserveSystem: future timestamp");
|
|
|
|
if (!isSupportedAsset[asset]) {
|
|
isSupportedAsset[asset] = true;
|
|
supportedAssets.push(asset);
|
|
}
|
|
|
|
priceFeeds[asset] = PriceFeed({
|
|
price: price,
|
|
timestamp: timestamp,
|
|
isValid: true
|
|
});
|
|
|
|
emit PriceFeedUpdated(asset, price, timestamp);
|
|
}
|
|
|
|
function getPrice(address asset) public view override returns (uint256 price, uint256 timestamp) {
|
|
PriceFeed memory feed = priceFeeds[asset];
|
|
require(feed.isValid, "ReserveSystem: price feed not available");
|
|
return (feed.price, feed.timestamp);
|
|
}
|
|
|
|
function getConversionPrice(
|
|
address sourceAsset,
|
|
address targetAsset
|
|
) external view override returns (uint256) {
|
|
(uint256 sourcePrice,) = getPrice(sourceAsset);
|
|
(uint256 targetPrice,) = getPrice(targetAsset);
|
|
return (targetPrice * 1e18) / sourcePrice; // Price in 18 decimals
|
|
}
|
|
|
|
// ============ Admin Functions ============
|
|
|
|
function addSupportedAsset(address asset, bool isLiquid) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
require(asset != address(0), "ReserveSystem: zero address");
|
|
isSupportedAsset[asset] = true;
|
|
isLiquidAsset[asset] = isLiquid;
|
|
if (!_isInArray(asset, supportedAssets)) {
|
|
supportedAssets.push(asset);
|
|
}
|
|
}
|
|
|
|
function removeSupportedAsset(address asset) external onlyRole(DEFAULT_ADMIN_ROLE) {
|
|
isSupportedAsset[asset] = false;
|
|
}
|
|
|
|
function _isInArray(address item, address[] memory array) internal pure returns (bool) {
|
|
for (uint256 i = 0; i < array.length; i++) {
|
|
if (array[i] == item) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function getSupportedAssets() external view returns (address[] memory) {
|
|
return supportedAssets;
|
|
}
|
|
}
|
|
|