Contract Name:
CMTDE_V3_TradingModule
Contract Source Code:
File 1 of 1 : CMTDE_V3_TradingModule
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
/**
* ═══════════════════════════════════════════════════════════════════════════
* CMTDE V3 TRADING MODULE - BUY/SELL & ORACLE INTEGRATION
* ═══════════════════════════════════════════════════════════════════════════
*
* @title CMTDE_V3_TradingModule
* @dev Handles token purchases, sales, and gold price oracle integration
* @notice Deploy with: (CMTDE_V3 address, ConfigRegistry address, XAU/USD Feed, Backup XAU Feed, ETH/USD Feed)
*
* Changes to this module require 24-hour timelock via CMTDE_V3.
*
* MAINNET CHAINLINK ORACLES:
* - XAU/USD (Gold): 0x214eD9Da11D2fbe465a6fc601a91E62EbEc1a0D6
* - ETH/USD: 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
*
* PRICING MODEL:
* - 100 CMTDE tokens = 1 oz of gold (XAU)
* - 1 CMTDE token = goldPrice / 100 (e.g., $4,313.49 / 100 = $43.13)
* - ETH/USD oracle converts user's ETH to USD value for calculations
* ═══════════════════════════════════════════════════════════════════════════
*/
interface AggregatorV3Interface {
function decimals() external view returns (uint8);
function latestRoundData() external view returns (
uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound
);
}
interface ICMTDE_V3_ConfigRegistry {
function minGoldPrice() external view returns (uint256);
function maxGoldPrice() external view returns (uint256);
function priceStalenessThreshold() external view returns (uint256);
function priceMarkup() external view returns (uint256);
function priceDiscount() external view returns (uint256);
function maxMintPerTx() external view returns (uint256);
function maxDailyMint() external view returns (uint256);
function maxPurchasePerTx() external view returns (uint256);
function maxDailyPurchase() external view returns (uint256);
function minPurchaseETH() external view returns (uint256);
function maxPurchaseETH() external view returns (uint256);
function maxSellPerTx() external view returns (uint256);
function maxDailySell() external view returns (uint256);
function minSellTokens() external view returns (uint256);
function minContractBalance() external view returns (uint256);
function purchaseEnabled() external view returns (bool);
function sellEnabled() external view returns (bool);
function manualPriceEnabled() external view returns (bool);
function emergencyFallbackEnabled() external view returns (bool);
}
interface ICMTDE_V3 {
function mint(address to, uint256 amount) external;
function moduleBurn(address account, uint256 amount) external;
function balanceOf(address account) external view returns (uint256);
}
contract CMTDE_V3_TradingModule {
// ═══════════════════════════════════════════════════════════════════
// STATE VARIABLES
// ═══════════════════════════════════════════════════════════════════
address public owner;
address public pendingOwner;
address public coreToken;
address public configRegistry;
AggregatorV3Interface public goldPriceFeed;
AggregatorV3Interface public backupPriceFeed;
AggregatorV3Interface public ethUsdPriceFeed;
uint256 public manualGoldPrice;
uint256 public manualPriceTimestamp;
uint256 public emergencyFallbackPrice;
// Daily limits tracking
uint256 public dailyPurchaseAmount;
uint256 public lastPurchaseResetTime;
uint256 public dailySellAmount;
uint256 public lastSellResetTime;
uint256 private constant MANUAL_PRICE_VALIDITY = 86400; // 24 hours
bool private _locked;
// ═══════════════════════════════════════════════════════════════════
// EVENTS
// ═══════════════════════════════════════════════════════════════════
event TokensPurchased(address indexed buyer, uint256 ethAmount, uint256 tokenAmount, uint256 tokenPrice);
event TokensSold(address indexed seller, uint256 tokenAmount, uint256 ethAmount, uint256 tokenPrice);
event LiquidityAdded(address indexed provider, uint256 amount);
event LiquidityWithdrawn(address indexed provider, uint256 amount);
event ManualPriceSet(uint256 price, uint256 timestamp);
event EmergencyFallbackSet(uint256 price);
event OracleUpdated(string indexed oracleType, address oldAddress, address newAddress);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
// ═══════════════════════════════════════════════════════════════════
// MODIFIERS
// ═══════════════════════════════════════════════════════════════════
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier nonReentrant() {
require(!_locked, "Reentrant call");
_locked = true;
_;
_locked = false;
}
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTOR
// ═══════════════════════════════════════════════════════════════════
constructor(
address _coreToken,
address _configRegistry,
address _goldPriceFeed,
address _backupPriceFeed,
address _ethUsdPriceFeed
) {
require(_coreToken != address(0), "Zero token");
require(_configRegistry != address(0), "Zero config");
require(_goldPriceFeed != address(0), "Zero gold feed");
require(_ethUsdPriceFeed != address(0), "Zero ETH feed");
owner = msg.sender;
coreToken = _coreToken;
configRegistry = _configRegistry;
goldPriceFeed = AggregatorV3Interface(_goldPriceFeed);
backupPriceFeed = AggregatorV3Interface(_backupPriceFeed);
ethUsdPriceFeed = AggregatorV3Interface(_ethUsdPriceFeed);
lastPurchaseResetTime = block.timestamp;
lastSellResetTime = block.timestamp;
}
// ═══════════════════════════════════════════════════════════════════
// PRICE ORACLE
// ═══════════════════════════════════════════════════════════════════
function getLatestGoldPrice() public view returns (uint256) {
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
// Check manual price first
if (config.manualPriceEnabled() && manualGoldPrice > 0) {
require(block.timestamp - manualPriceTimestamp <= MANUAL_PRICE_VALIDITY, "Manual price stale");
return manualGoldPrice;
}
return _getValidatedPrice(goldPriceFeed, config);
}
function _getValidatedPrice(AggregatorV3Interface priceFeed, ICMTDE_V3_ConfigRegistry config) internal view returns (uint256) {
(
uint80 roundId,
int256 price,
,
uint256 timeStamp,
uint80 answeredInRound
) = priceFeed.latestRoundData();
require(timeStamp > 0, "Invalid timestamp");
require(answeredInRound >= roundId, "Stale round");
require(block.timestamp - timeStamp <= config.priceStalenessThreshold(), "Oracle stale");
require(price > 0, "Invalid price");
uint256 uPrice = uint256(price);
require(uPrice >= config.minGoldPrice(), "Price below minimum");
require(uPrice <= config.maxGoldPrice(), "Price above maximum");
return uPrice;
}
function getGoldPriceWithFallback() public view returns (uint256) {
try this.getLatestGoldPrice() returns (uint256 price) {
return price;
} catch {
// Try backup feed
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
try this._tryBackupFeed() returns (uint256 backupPrice) {
return backupPrice;
} catch {
// Emergency fallback
require(config.emergencyFallbackEnabled(), "No fallback enabled");
require(emergencyFallbackPrice > 0, "No fallback price");
return emergencyFallbackPrice;
}
}
}
function _tryBackupFeed() external view returns (uint256) {
return _getValidatedPrice(backupPriceFeed, ICMTDE_V3_ConfigRegistry(configRegistry));
}
/**
* @dev Get current ETH/USD price from Chainlink
* @return ETH price in USD (8 decimals)
*/
function getEthUsdPrice() public view returns (uint256) {
(
uint80 roundId,
int256 price,
,
uint256 timeStamp,
uint80 answeredInRound
) = ethUsdPriceFeed.latestRoundData();
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
require(timeStamp > 0, "ETH: Invalid timestamp");
require(answeredInRound >= roundId, "ETH: Stale round");
require(block.timestamp - timeStamp <= config.priceStalenessThreshold(), "ETH: Oracle stale");
require(price > 0, "ETH: Invalid price");
uint256 uPrice = uint256(price);
// Sanity bounds for ETH: $100 - $100,000
require(uPrice >= 100 * 10**8, "ETH: Price too low");
require(uPrice <= 100000 * 10**8, "ETH: Price too high");
return uPrice;
}
/**
* @dev Convert ETH amount (wei) to USD value (8 decimals)
* @param ethAmount Amount of ETH in wei
* @return USD value with 8 decimals
*/
function ethToUsd(uint256 ethAmount) public view returns (uint256) {
uint256 ethPrice = getEthUsdPrice(); // 8 decimals
// ethAmount (18 decimals) * ethPrice (8 decimals) / 1e18 = USD (8 decimals)
return (ethAmount * ethPrice) / 1e18;
}
/**
* @dev Convert USD amount (8 decimals) to ETH value (wei)
* @param usdAmount Amount of USD with 8 decimals
* @return ETH value in wei (18 decimals)
*/
function usdToEth(uint256 usdAmount) public view returns (uint256) {
uint256 ethPrice = getEthUsdPrice(); // 8 decimals
// usdAmount (8 decimals) * 1e18 / ethPrice (8 decimals) = ETH (18 decimals)
return (usdAmount * 1e18) / ethPrice;
}
/**
* @dev Get current token value based on gold price
* @return Price per 1 token (8 decimals)
* @notice 100 tokens = 1 oz gold, so 1 token = goldPrice / 100
*/
function getCurrentTokenValue() public view returns (uint256) {
uint256 goldPrice = getGoldPriceWithFallback();
return (goldPrice + 50) / 100; // Price per 1 token, rounded
}
/**
* @dev Get token purchase price with markup
* @return Price per 1 token (8 decimals)
*/
function getTokenPurchasePrice() public view returns (uint256) {
uint256 basePrice = getCurrentTokenValue();
uint256 markup = ICMTDE_V3_ConfigRegistry(configRegistry).priceMarkup();
return (basePrice * markup) / 100;
}
/**
* @dev Get token sell price with discount
* @return Price per 1 token (8 decimals)
*/
function getTokenSellPrice() public view returns (uint256) {
uint256 basePrice = getCurrentTokenValue();
uint256 discount = ICMTDE_V3_ConfigRegistry(configRegistry).priceDiscount();
return (basePrice * discount) / 100;
}
// ═══════════════════════════════════════════════════════════════════
// BUY/SELL FUNCTIONS
// ═══════════════════════════════════════════════════════════════════
/**
* @dev Buy tokens with ETH
* @notice Converts ETH to USD value, then calculates tokens based on gold price
*
* Formula:
* 1. usdValue = ETH amount × ETH/USD price
* 2. tokens = usdValue / tokenPrice
*/
function buyTokens() external payable nonReentrant {
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
require(config.purchaseEnabled(), "Purchases disabled");
require(msg.value >= config.minPurchaseETH(), "Below min ETH");
require(msg.value <= config.maxPurchaseETH(), "Above max ETH");
// Reset daily counter if needed
if (block.timestamp - lastPurchaseResetTime >= 86400) {
dailyPurchaseAmount = 0;
lastPurchaseResetTime = block.timestamp;
}
// Convert ETH to USD value (8 decimals)
uint256 usdValue = ethToUsd(msg.value);
// Get token price with markup (8 decimals, price per 1 token)
uint256 tokenPrice = getTokenPurchasePrice();
// Calculate tokens: (usdValue * 1e18) / tokenPrice
// usdValue (8 dec) * 1e18 / tokenPrice (8 dec) = tokens (18 dec)
uint256 tokensAmount = (usdValue * 1e18) / tokenPrice;
require(tokensAmount > 0, "Zero tokens");
require(tokensAmount <= config.maxPurchasePerTx(), "Exceeds tx limit");
require(dailyPurchaseAmount + tokensAmount <= config.maxDailyPurchase(), "Exceeds daily limit");
dailyPurchaseAmount += tokensAmount;
ICMTDE_V3(coreToken).mint(msg.sender, tokensAmount);
emit TokensPurchased(msg.sender, msg.value, tokensAmount, tokenPrice);
}
/**
* @dev Sell tokens for ETH
* @param tokenAmount Number of tokens to sell (18 decimals)
* @notice Converts token value to USD, then to ETH
*
* Formula:
* 1. usdValue = tokens × tokenPrice
* 2. ethAmount = usdValue / ETH price
*/
function sellTokens(uint256 tokenAmount) external nonReentrant {
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
require(config.sellEnabled(), "Sales disabled");
require(tokenAmount >= config.minSellTokens(), "Below min tokens");
require(tokenAmount <= config.maxSellPerTx(), "Above max tokens");
require(ICMTDE_V3(coreToken).balanceOf(msg.sender) >= tokenAmount, "Insufficient balance");
// Reset daily counter if needed
if (block.timestamp - lastSellResetTime >= 86400) {
dailySellAmount = 0;
lastSellResetTime = block.timestamp;
}
require(dailySellAmount + tokenAmount <= config.maxDailySell(), "Exceeds daily limit");
// Get token price with discount (8 decimals, price per 1 token)
uint256 tokenPrice = getTokenSellPrice();
// Calculate USD value of tokens: (tokenAmount * tokenPrice) / 1e18
// tokenAmount (18 dec) * tokenPrice (8 dec) / 1e18 = usdValue (8 dec)
uint256 usdValue = (tokenAmount * tokenPrice) / 1e18;
// Convert USD value to ETH
uint256 ethAmount = usdToEth(usdValue);
require(address(this).balance >= ethAmount + config.minContractBalance(), "Insufficient liquidity");
dailySellAmount += tokenAmount;
ICMTDE_V3(coreToken).moduleBurn(msg.sender, tokenAmount);
(bool success, ) = payable(msg.sender).call{value: ethAmount}("");
require(success, "ETH transfer failed");
emit TokensSold(msg.sender, tokenAmount, ethAmount, tokenPrice);
}
// ═══════════════════════════════════════════════════════════════════
// PEG STATUS & ARBITRAGE
// ═══════════════════════════════════════════════════════════════════
/**
* @dev Get comprehensive peg status
*/
function getPegStatus() external view returns (
uint256 oracleGoldPrice,
uint256 contractBuyPrice,
uint256 contractSellPrice,
uint256 spreadBps,
uint256 ethLiquidity,
bool canAcceptSells,
uint256 maxBuyableTokens
) {
oracleGoldPrice = getGoldPriceWithFallback();
contractBuyPrice = getTokenPurchasePrice();
contractSellPrice = getTokenSellPrice();
if (contractBuyPrice > 0) {
spreadBps = ((contractBuyPrice - contractSellPrice) * 10000) / contractBuyPrice;
}
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
ethLiquidity = address(this).balance;
uint256 minBalance = config.minContractBalance();
canAcceptSells = ethLiquidity > minBalance;
if (canAcceptSells && contractSellPrice > 0) {
// Convert available ETH to USD, then calculate max tokens
uint256 availableUsd = ethToUsd(ethLiquidity - minBalance);
maxBuyableTokens = (availableUsd * 1e18) / contractSellPrice;
}
}
/**
* @dev Calculate arbitrage opportunity
* @param dexPrice Current DEX price (8 decimals, per 1 token in USD)
*/
function calculateArbitrageOpportunity(uint256 dexPrice) external view returns (
uint8 arbDirection,
uint256 profitBps,
uint256 optimalAmount,
string memory arbDescription
) {
uint256 contractBuyPrice = getTokenPurchasePrice();
uint256 contractSellPrice = getTokenSellPrice();
if (dexPrice == 0 || contractBuyPrice == 0 || contractSellPrice == 0) {
return (0, 0, 0, "Invalid prices");
}
// Buy on DEX, sell to contract
if (dexPrice < contractSellPrice) {
profitBps = ((contractSellPrice - dexPrice) * 10000) / dexPrice;
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
uint256 availableETH = address(this).balance;
uint256 minBalance = config.minContractBalance();
if (availableETH > minBalance) {
// Convert available ETH to USD, then calculate max tokens
uint256 availableUsd = ethToUsd(availableETH - minBalance);
uint256 maxSellable = (availableUsd * 1e18) / contractSellPrice;
uint256 dailyLimit = config.maxSellPerTx();
optimalAmount = maxSellable < dailyLimit ? maxSellable : dailyLimit;
}
return (1, profitBps, optimalAmount, "Buy on DEX, sell to contract");
}
// Buy from contract, sell on DEX
if (dexPrice > contractBuyPrice) {
profitBps = ((dexPrice - contractBuyPrice) * 10000) / contractBuyPrice;
optimalAmount = ICMTDE_V3_ConfigRegistry(configRegistry).maxPurchasePerTx();
return (2, profitBps, optimalAmount, "Buy from contract, sell on DEX");
}
return (0, 0, 0, "No profitable arbitrage");
}
/**
* @dev Check if DEX price is within tolerance of oracle price
*/
function checkPegHealth(uint256 dexPrice, uint256 toleranceBps) external view returns (
bool isPegged,
uint256 deviationBps,
uint8 deviationDirection
) {
uint256 oraclePrice = getCurrentTokenValue();
if (dexPrice == 0 || oraclePrice == 0) {
return (false, 10000, 0);
}
if (dexPrice >= oraclePrice) {
deviationBps = ((dexPrice - oraclePrice) * 10000) / oraclePrice;
deviationDirection = 2; // DEX above oracle
} else {
deviationBps = ((oraclePrice - dexPrice) * 10000) / oraclePrice;
deviationDirection = 1; // DEX below oracle
}
isPegged = deviationBps <= toleranceBps;
}
/**
* @dev Simulate a buy to preview results
*/
function simulateBuy(uint256 ethAmount) external view returns (
uint256 tokensOut,
uint256 effectivePrice,
bool withinLimits
) {
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
if (ethAmount < config.minPurchaseETH() || ethAmount > config.maxPurchaseETH()) {
return (0, 0, false);
}
// Convert ETH to USD
uint256 usdValue = ethToUsd(ethAmount);
effectivePrice = getTokenPurchasePrice(); // Price per 1 token (8 decimals)
tokensOut = (usdValue * 1e18) / effectivePrice;
uint256 remainingDaily = config.maxDailyPurchase() - dailyPurchaseAmount;
withinLimits = tokensOut <= config.maxPurchasePerTx() && tokensOut <= remainingDaily;
}
/**
* @dev Simulate a sell to preview results
*/
function simulateSell(uint256 tokenAmount) external view returns (
uint256 ethOut,
uint256 effectivePrice,
bool withinLimits
) {
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
if (tokenAmount < config.minSellTokens() || tokenAmount > config.maxSellPerTx()) {
return (0, 0, false);
}
effectivePrice = getTokenSellPrice(); // Price per 1 token (8 decimals)
// Calculate USD value then convert to ETH
uint256 usdValue = (tokenAmount * effectivePrice) / 1e18;
ethOut = usdToEth(usdValue);
uint256 remainingDaily = config.maxDailySell() - dailySellAmount;
bool withinDailyLimit = tokenAmount <= remainingDaily;
bool hasLiquidity = address(this).balance >= ethOut + config.minContractBalance();
withinLimits = withinDailyLimit && hasLiquidity;
}
// ═══════════════════════════════════════════════════════════════════
// LIQUIDITY MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
function addLiquidity() external payable onlyOwner {
require(msg.value > 0, "No ETH sent");
emit LiquidityAdded(msg.sender, msg.value);
}
function withdrawETH(uint256 amount) external onlyOwner {
require(amount <= address(this).balance, "Insufficient balance");
(bool success, ) = payable(owner).call{value: amount}("");
require(success, "ETH transfer failed");
emit LiquidityWithdrawn(owner, amount);
}
function getETHBalance() external view returns (uint256) {
return address(this).balance;
}
receive() external payable {
require(msg.sender == owner, "Only owner can send ETH directly");
}
// ═══════════════════════════════════════════════════════════════════
// ORACLE MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
function setManualGoldPrice(uint256 price) external onlyOwner {
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
require(price >= config.minGoldPrice(), "Price below minimum");
require(price <= config.maxGoldPrice(), "Price above maximum");
manualGoldPrice = price;
manualPriceTimestamp = block.timestamp;
emit ManualPriceSet(price, block.timestamp);
}
function clearManualPrice() external onlyOwner {
manualGoldPrice = 0;
manualPriceTimestamp = 0;
}
function setEmergencyFallbackPrice(uint256 price) external onlyOwner {
ICMTDE_V3_ConfigRegistry config = ICMTDE_V3_ConfigRegistry(configRegistry);
require(price >= config.minGoldPrice(), "Price below minimum");
require(price <= config.maxGoldPrice(), "Price above maximum");
emergencyFallbackPrice = price;
emit EmergencyFallbackSet(price);
}
function setPrimaryOracle(address _oracle) external onlyOwner {
require(_oracle != address(0), "Zero address");
address old = address(goldPriceFeed);
goldPriceFeed = AggregatorV3Interface(_oracle);
emit OracleUpdated("Primary", old, _oracle);
}
function setBackupOracle(address _oracle) external onlyOwner {
address old = address(backupPriceFeed);
backupPriceFeed = AggregatorV3Interface(_oracle);
emit OracleUpdated("Backup", old, _oracle);
}
function setEthUsdOracle(address _oracle) external onlyOwner {
require(_oracle != address(0), "Zero address");
address old = address(ethUsdPriceFeed);
ethUsdPriceFeed = AggregatorV3Interface(_oracle);
emit OracleUpdated("ETH/USD", old, _oracle);
}
// ═══════════════════════════════════════════════════════════════════
// OWNERSHIP (2-step)
// ═══════════════════════════════════════════════════════════════════
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "Zero address");
pendingOwner = newOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "Not pending owner");
address oldOwner = owner;
owner = pendingOwner;
pendingOwner = address(0);
emit OwnershipTransferred(oldOwner, owner);
}
}