ETH Price: $2,070.31 (-2.34%)

Contract Diff Checker

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);
    }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):