Stele

Description:

Decentralized Finance (DeFi) protocol contract providing Swap, Factory functionality.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 500
    },
    "viaIR": true,
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  },
  "sources": {
    "contracts/Stele.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import './interfaces/IERC20Minimal.sol';
import './interfaces/IStele.sol';
import {PriceOracle, IUniswapV3Factory} from './libraries/PriceOracle.sol';

struct Token {
  address tokenAddress;
  uint256 amount;
}

struct UserPortfolio {
  Token[] tokens;
}

struct Challenge {
  uint256 id;
  IStele.ChallengeType challengeType;
  uint256 startTime;
  uint256 endTime;
  uint256 totalRewards; // USD Token
  uint256 seedMoney;
  uint256 entryFee;
  uint32 totalUsers;
  address[5] topUsers; // top 5 users
  uint256[5] scores; // scores of top 5 users
  mapping(address => UserPortfolio) portfolios;
}

// Interface for StelePerformanceNFT contract
interface IStelePerformanceNFT {
  function mintPerformanceNFT(
    uint256 challengeId,
    address user,
    uint32 totalUsers,
    uint256 finalScore,
    uint8 rank,
    uint256 initialValue,
    IStele.ChallengeType challengeType,
    uint256 challengeStartTime
  ) external returns (uint256);
  
  function canMintNFT(uint256 challengeId, address user) external view returns (bool);
}

contract Stele is IStele {
  using PriceOracle for *;
  
  address public constant uniswapV3Factory = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
  
  // State variables
  address public override owner;
  address public override usdToken;
  address public override weth9;
  uint256 public override seedMoney;
  uint256 public override entryFee;
  uint8 public override usdTokenDecimals;
  uint8 public override maxTokens;
  uint256[5] public override rewardRatio;
  mapping(address => bool) public override isInvestable;
  mapping(uint256 => bool) public override rewardsDistributed;

  // Stele Token Bonus System
  address public override steleToken;
  uint256 public override createBonus;
  uint256 public override joinBonus;
  uint256 public override getRewardsBonus;

  // Challenge repository
  mapping(uint256 => Challenge) public challenges;
  uint256 public override challengeCounter;
  // Latest challenge ID by challenge type
  mapping(IStele.ChallengeType => uint256) public override latestChallengesByType;

  // NFT contract address
  address public override performanceNFTContract;

  modifier onlyOwner() {
      require(msg.sender == owner, 'NO');
      _;
  }
  
  // Contract constructor
  constructor(address _weth9, address _usdToken, address _steleToken) {
    owner = msg.sender;
    usdToken = _usdToken;
    weth9 = _weth9;
    usdTokenDecimals = IERC20Minimal(_usdToken).decimals(); 
    maxTokens = 10;
    seedMoney = 1000 * 10**usdTokenDecimals;
    entryFee = 10 * 10**usdTokenDecimals; // 10 USD
    rewardRatio = [50, 26, 13, 7, 4];
    challengeCounter = 0;
    // Initialize Stele Token Bonus
    steleToken = _steleToken;
    createBonus = 1000 * 10**18; // 1000 STL tokens
    joinBonus = 500 * 10**18; // 500 STL tokens
    getRewardsBonus = 50000 * 10**18; // 50000 STL tokens

    // Initialize investable tokens directly
    isInvestable[weth9] = true;
    emit AddToken(weth9);
    isInvestable[usdToken] = true;
    emit AddToken(usdToken);

    emit SteleCreated(owner, usdToken, maxTokens, seedMoney, entryFee, rewardRatio);
  }

  // Transfer ownership of the contract to a new account
  function transferOwnership(address newOwner) external override onlyOwner {
    require(newOwner != address(0), "NZ");
    emit OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }

  // Set Performance NFT contract address
  function setPerformanceNFTContract(address _nftContract) external override onlyOwner {
    require(_nftContract != address(0), "NZ");
    performanceNFTContract = _nftContract;
    emit PerformanceNFTContractSet(_nftContract);
  }

  // Duration in seconds for each challenge type
  function getDuration(IStele.ChallengeType challengeType) internal pure returns (uint256) {
    if (challengeType == IStele.ChallengeType.OneWeek) return 7 days;
    if (challengeType == IStele.ChallengeType.OneMonth) return 30 days;
    if (challengeType == IStele.ChallengeType.ThreeMonths) return 90 days;
    if (challengeType == IStele.ChallengeType.SixMonths) return 180 days;
    if (challengeType == IStele.ChallengeType.OneYear) return 365 days;
    return 0;
  }

  // Reward distribution ratio setting function
  function setRewardRatio(uint256[5] calldata _rewardRatio) external override onlyOwner {
    uint256 sum = 0;
    for (uint i = 0; i < 5; i++) {
        require(_rewardRatio[i] > 0, "IR");
        sum += _rewardRatio[i];
    }
    require(sum == 100, "IS");
    
    // Ensure reward ratio is in descending order (1st > 2nd > 3rd > 4th > 5th)
    for (uint i = 0; i < 4; i++) {
        require(_rewardRatio[i] > _rewardRatio[i + 1], "RD"); // Reward ratio must be Descending
    }
    
    rewardRatio = _rewardRatio;
    emit RewardRatio(_rewardRatio);
  }
  
  // Entry fee setting function
  function setEntryFee(uint256 _entryFee) external override onlyOwner {
    entryFee = _entryFee;
    emit EntryFee(_entryFee);
  }
  
  // Initial capital setting function
  function setSeedMoney(uint256 _seedMoney) external override onlyOwner {
    seedMoney = _seedMoney;
    emit SeedMoney(_seedMoney);
  }
  
  // Investable token setting function
  function setToken(address tokenAddress) external override onlyOwner {
    require(tokenAddress != address(0), "ZA"); // Zero Address
    require(!isInvestable[tokenAddress], "AT"); // Already Token
    
    isInvestable[tokenAddress] = true;
    emit AddToken(tokenAddress);
  }
  
  // Non-investable token setting function
  function resetToken(address tokenAddress) external override onlyOwner {
    require(tokenAddress != address(0), "ZA"); // Zero Address
    require(isInvestable[tokenAddress], "NT"); // Not investableToken
    require(tokenAddress != usdToken, "UCR"); // USD token Cannot be Removed
    require(tokenAddress != weth9, "WCR"); // WETH Cannot be Removed

    isInvestable[tokenAddress] = false;
    emit RemoveToken(tokenAddress);
  }

  // Max tokens setting function
  function setMaxTokens(uint8 _maxTokens) external override onlyOwner {
    maxTokens = _maxTokens;
    emit MaxTokens(_maxTokens);
  }

  // Create challenge bonus setting function
  function setCreateBonus(uint256 _createBonus) external onlyOwner {
    createBonus = _createBonus;
    emit CreateBonusUpdated(_createBonus);
  }

  // Join challenge bonus setting function
  function setJoinBonus(uint256 _joinBonus) external onlyOwner {
    joinBonus = _joinBonus;
    emit JoinBonusUpdated(_joinBonus);
  }

  // Get rewards bonus setting function
  function setGetRewardsBonus(uint256 _getRewardsBonus) external onlyOwner {
    getRewardsBonus = _getRewardsBonus;
    emit GetRewardsBonusUpdated(_getRewardsBonus);
  }

  // Get challenge basic info (cannot return mappings in interface)
  function getChallengeInfo(uint256 challengeId) external view override returns (
    uint256 _id,
    IStele.ChallengeType _challengeType,
    uint256 _startTime,
    uint256 _endTime,
    uint256 _totalRewards,
    uint256 _seedMoney,
    uint256 _entryFee,
    uint32 _totalUsers
  ) {
    Challenge storage challenge = challenges[challengeId];
    return (
      challenge.id,
      challenge.challengeType,
      challenge.startTime,
      challenge.endTime,
      challenge.totalRewards,
      challenge.seedMoney,
      challenge.entryFee,
      challenge.totalUsers
    );
  }

  // Get user's portfolio in a specific challenge
  function getUserPortfolio(uint256 challengeId, address user) external view override returns (address[] memory tokenAddresses, uint256[] memory amounts) {
    Challenge storage challenge = challenges[challengeId];
    require(challenge.startTime > 0, "CNE");
    
    UserPortfolio memory portfolio = challenge.portfolios[user];
    uint256 tokenCount = portfolio.tokens.length;

    tokenAddresses = new address[](tokenCount);
    amounts = new uint256[](tokenCount);

    for (uint256 i = 0; i < tokenCount; i++) {
      tokenAddresses[i] = portfolio.tokens[i].tokenAddress;
      amounts[i] = portfolio.tokens[i].amount;
    }
    
    return (tokenAddresses, amounts);
  }


  // Create a new challenge
  function createChallenge(IStele.ChallengeType challengeType) external override {
    uint256 latestChallengeId = latestChallengesByType[challengeType];
    // Only allow creating a new challenge if it's the first challenge or the previous challenge has ended
    if (latestChallengeId != 0) {
      require(block.timestamp > challenges[latestChallengeId].endTime, "NE");
    }

    challengeCounter++;
    uint256 challengeId = challengeCounter;

    // Update latest challenge for this type
    latestChallengesByType[challengeType] = challengeId;

    Challenge storage challenge = challenges[challengeId];
    challenge.id = challengeId;
    challenge.challengeType = challengeType;
    challenge.startTime = block.timestamp;
    challenge.endTime = block.timestamp + getDuration(challengeType);
    challenge.totalRewards = 0;
    challenge.seedMoney = seedMoney;
    challenge.entryFee = entryFee;
    challenge.totalUsers = 0;
    
    // Initialize top users and their values
    for (uint i = 0; i < 5; i++) {
      challenge.topUsers[i] = address(0);
      challenge.scores[i] = 0;
    }
    
    emit Create(challengeId, challengeType, challenge.seedMoney, challenge.entryFee);
    
    // Distribute Stele token bonus for creating challenge
    distributeSteleBonus(challengeId, msg.sender, createBonus, "CR");
  }

  // Join an existing challenge
  function joinChallenge(uint256 challengeId) external override {
    Challenge storage challenge = challenges[challengeId];
    
    // Check if challenge exists and is still active
    require(challenge.startTime > 0, "CNE");
    require(block.timestamp < challenge.endTime, "E");
    
    // Check if user has already joined
    require(challenge.portfolios[msg.sender].tokens.length == 0, "AJ");
    
    // Transfer USD token to contract
    IERC20Minimal usdTokenContract = IERC20Minimal(usdToken);
    
    // First check if user has enough tokens
    require(usdTokenContract.balanceOf(msg.sender) >= challenge.entryFee, "NEB");
    
    // Check if user has approved the contract to transfer tokens
    require(usdTokenContract.allowance(msg.sender, address(this)) >= challenge.entryFee, "NA");
    
    // Transfer tokens
    bool transferSuccess = usdTokenContract.transferFrom(msg.sender, address(this), challenge.entryFee);
    require(transferSuccess, "TF");
    
    // Add user to challenge
    UserPortfolio storage portfolio = challenge.portfolios[msg.sender];
    
    // Initialize with seed money in USD
    Token memory initialToken = Token({
      tokenAddress: usdToken,
      amount: challenge.seedMoney
    });

    portfolio.tokens.push(initialToken);

    // Update challenge total rewards
    challenge.totalRewards = challenge.totalRewards + challenge.entryFee;
    challenge.totalUsers = uint32(challenge.totalUsers + 1);

    emit Join(challengeId, msg.sender, challenge.seedMoney);

    // Distribute Stele token bonus for joining challenge
    distributeSteleBonus(challengeId, msg.sender, joinBonus, "JCR");

    register(challengeId); // Auto-register after joining to update ranking
  }

  // Swap tokens within a challenge portfolio
  function swap(uint256 challengeId, address tokenIn, address tokenOut, uint256 amount) external override {
    Challenge storage challenge = challenges[challengeId];
    
    // Validate challenge and user
    require(challenge.startTime > 0, "CNE");
    require(block.timestamp < challenge.endTime, "E");
    
    // Validate tokens
    require(tokenIn != tokenOut, "ST"); // Prevent same token swap
    require(isInvestable[tokenOut], "IT"); // Not investableToken
    
    // Get user portfolio
    UserPortfolio storage portfolio = challenge.portfolios[msg.sender];
    require(portfolio.tokens.length > 0, "UNE");
    
    // Find the source token in portfolio
    bool found = false;
    uint256 index;
    for (uint256 i = 0; i < portfolio.tokens.length; i++) {
      if (portfolio.tokens[i].tokenAddress == tokenIn) {
        require(portfolio.tokens[i].amount >= amount, "FTM");
        index = i;
        found = true;
        break;
      }
    }
    
    require(found, "ANE");

    // Get token prices using ETH as intermediate
    uint8 tokenInDecimals = IERC20Minimal(tokenIn).decimals();
    uint8 tokenOutDecimals = IERC20Minimal(tokenOut).decimals();

    uint256 tokenInPriceUSD;
    uint256 tokenOutPriceUSD;

    // Calculate tokenInPriceUSD using ETH as intermediate
    if (tokenIn == usdToken) {
      tokenInPriceUSD = 1 * 10 ** usdTokenDecimals;
    } else if (tokenIn == weth9) {
      tokenInPriceUSD = PriceOracle.getETHPriceUSD(uniswapV3Factory, weth9, usdToken);
    } else {
      tokenInPriceUSD = (PriceOracle.getTokenPriceETH(uniswapV3Factory, tokenIn, weth9, uint128(1 * 10 ** tokenInDecimals)) * PriceOracle.getETHPriceUSD(uniswapV3Factory, weth9, usdToken)) / 10 ** 18;
    }

    // Calculate tokenOutPriceUSD using ETH as intermediate
    if (tokenOut == usdToken) {
      tokenOutPriceUSD = 1 * 10 ** usdTokenDecimals;
    } else if (tokenOut == weth9) {
      tokenOutPriceUSD = PriceOracle.getETHPriceUSD(uniswapV3Factory, weth9, usdToken);
    } else {
      tokenOutPriceUSD = (PriceOracle.getTokenPriceETH(uniswapV3Factory, tokenOut, weth9, uint128(1 * 10 ** tokenOutDecimals)) * PriceOracle.getETHPriceUSD(uniswapV3Factory, weth9, usdToken)) / 10 ** 18;
    }
        
    // Validate that prices are available
    require(tokenInPriceUSD > 0, "FP0");
    require(tokenOutPriceUSD > 0, "TP0");

    // Calculate swap amount with decimal adjustment
    uint256 toAmount = (amount * tokenInPriceUSD) / tokenOutPriceUSD;

    // Adjust for decimal differences
    if (tokenOutDecimals > tokenInDecimals) {
      toAmount = toAmount * 10 ** (tokenOutDecimals - tokenInDecimals);
    } else if (tokenInDecimals > tokenOutDecimals) {
      toAmount = toAmount / 10 ** (tokenInDecimals - tokenOutDecimals);
    }
    
    // Ensure swap amount is not zero
    require(toAmount > 0, "TA0");
    
    // Update source token balance
    portfolio.tokens[index].amount = portfolio.tokens[index].amount - amount;

    // Add or update target token balance
    bool foundTarget = false;

    for (uint256 i = 0; i < portfolio.tokens.length; i++) {
      if (portfolio.tokens[i].tokenAddress == tokenOut) {
        portfolio.tokens[i].amount = portfolio.tokens[i].amount + toAmount;
        foundTarget = true;
        break;
      }
    }
    
    if (!foundTarget) {
      require(portfolio.tokens.length < maxTokens, "FA");
      portfolio.tokens.push(Token({
        tokenAddress: tokenOut,
        amount: toAmount
      }));
    }
    
    // Remove token if balance is zero
    if (portfolio.tokens[index].amount == 0) {
      // Only reorganize array if not already the last element
      if (index != portfolio.tokens.length - 1) {
        portfolio.tokens[index] = portfolio.tokens[portfolio.tokens.length - 1];
      }
      portfolio.tokens.pop();
    }

    emit Swap(challengeId, msg.sender, tokenIn, tokenOut, amount, toAmount);

    register(challengeId); // Auto-register after swap to update ranking
  }

  // Register latest performance
  function register(uint256 challengeId) public override {
    Challenge storage challenge = challenges[challengeId];
    
    // Validate challenge and user
    require(challenge.startTime > 0, "CNE");
    require(block.timestamp < challenge.endTime, "E");
    
    // Calculate total portfolio value USD using ETH as intermediate
    uint256 userScore = 0;
    uint256 ethPriceUSD = PriceOracle.getETHPriceUSD(uniswapV3Factory, weth9, usdToken); // Get ETH price once for efficiency
    
    UserPortfolio memory portfolio = challenge.portfolios[msg.sender];
    for (uint256 i = 0; i < portfolio.tokens.length; i++) {
      address tokenAddress = portfolio.tokens[i].tokenAddress;
      uint8 _tokenDecimals = IERC20Minimal(tokenAddress).decimals();

      if(!isInvestable[tokenAddress]) continue;

      uint256 tokenPriceUSD;
      if (tokenAddress == usdToken) {
        tokenPriceUSD = 1 * 10 ** usdTokenDecimals;
      } else if (tokenAddress == weth9) {
        tokenPriceUSD = ethPriceUSD;
      } else {
        uint256 tokenPriceETH = PriceOracle.getTokenPriceETH(uniswapV3Factory, tokenAddress, weth9, uint128(1 * 10 ** _tokenDecimals));
        tokenPriceUSD = (tokenPriceETH * ethPriceUSD) / 10 ** 18;
      }

      uint256 tokenValueUSD = (portfolio.tokens[i].amount * tokenPriceUSD) / 10 ** _tokenDecimals;
      userScore = userScore + tokenValueUSD;
    }
    
    // Update ranking
    updateRanking(challengeId, msg.sender, userScore);
    
    emit Register(challengeId, msg.sender, userScore);    
  }

  // Helper function to update top performers (optimized)
  function updateRanking(uint256 challengeId, address user, uint256 userScore) internal {
    Challenge storage challenge = challenges[challengeId];
    
    // Check if user is already in top performers
    int256 existingIndex = -1;
    
    for (uint256 i = 0; i < 5; i++) {
      if (challenge.topUsers[i] == user) {
        existingIndex = int256(i);
        break;
      }
    }
    
    if (existingIndex >= 0) {
      // User already exists - remove and reinsert
      uint256 idx = uint256(existingIndex);
      
      // Shift elements to remove current position
      for (uint256 i = idx; i < 4; i++) {
        challenge.topUsers[i] = challenge.topUsers[i + 1];
        challenge.scores[i] = challenge.scores[i + 1];
      }
      
      // Clear last position
      challenge.topUsers[4] = address(0);
      challenge.scores[4] = 0;
    }
    
    // Find insertion position using binary search concept (for sorted array)
    uint256 insertPos = 5; // Default: not in top 5
    
    for (uint256 i = 0; i < 5; i++) {
      if (challenge.topUsers[i] == address(0) || userScore > challenge.scores[i]) {
        insertPos = i;
        break;
      }
    }
    
    // Insert if position found
    if (insertPos < 5) {
      // Shift elements to make space
      for (uint256 i = 4; i > insertPos; i--) {
        challenge.topUsers[i] = challenge.topUsers[i - 1];
        challenge.scores[i] = challenge.scores[i - 1];
      }
      
      // Insert new entry
      challenge.topUsers[insertPos] = user;
      challenge.scores[insertPos] = userScore;
    }
  }
  
  function getRanking(uint256 challengeId) external view override returns (address[5] memory topUsers, uint256[5] memory scores) {
    Challenge storage challenge = challenges[challengeId];
    for (uint256 i = 0; i < 5; i++) {
      topUsers[i] = challenge.topUsers[i];
      scores[i] = challenge.scores[i];
    }
  }

  // Claim rewards after challenge ends
  function getRewards(uint256 challengeId) external override {
    Challenge storage challenge = challenges[challengeId];
    // Validate challenge
    require(challenge.startTime > 0, "CNE");
    require(block.timestamp >= challenge.endTime, "NE");
    require(!rewardsDistributed[challengeId], "AD");

    // Mark as distributed first to prevent reentrancy
    rewardsDistributed[challengeId] = true;
    
    // Rewards distribution to top 5 participants
    uint256 undistributed = challenge.totalRewards;
    IERC20Minimal usdTokenContract = IERC20Minimal(usdToken);
    
    // Check USD token balance of the contract
    uint256 balance = usdTokenContract.balanceOf(address(this));
    require(balance >= undistributed, "NBR");
    
    // Calculate actual ranker count and initial rewards
    uint8 actualRankerCount = 0;
    address[5] memory validRankers;
    uint256[5] memory initialRewards;
    uint256 totalInitialRewardWeight = 0;
    
    for (uint8 i = 0; i < 5; i++) {
      address userAddress = challenge.topUsers[i];
      if (userAddress != address(0)) {
        validRankers[actualRankerCount] = userAddress;
        initialRewards[actualRankerCount] = rewardRatio[i];
        totalInitialRewardWeight = totalInitialRewardWeight + rewardRatio[i];
        actualRankerCount++;
      }
    }
    
    // Only distribute rewards if there are actual rankers
    if (actualRankerCount > 0) {
      // Distribute rewards to each ranker
      for (uint8 i = 0; i < actualRankerCount; i++) {
        address userAddress = validRankers[i];
        
        // Calculate reward based on original ratio
        require(totalInitialRewardWeight > 0, "IW");
        // Use direct calculation to avoid precision loss
        uint256 rewardAmount = (challenge.totalRewards * initialRewards[i]) / totalInitialRewardWeight;
        
        // Cannot distribute more than the available balance
        if (rewardAmount > undistributed) {
          rewardAmount = undistributed;
        }
        
        if (rewardAmount > 0) {
          // Update state before external call (Checks-Effects-Interactions pattern)
          undistributed = undistributed - rewardAmount;
          
          bool success = usdTokenContract.transfer(userAddress, rewardAmount);
          require(success, "RTF");

          emit Reward(challengeId, userAddress, rewardAmount);
          
          // Distribute Stele token bonus to each ranker
          distributeSteleBonus(challengeId, userAddress, getRewardsBonus, "RW");
        }
      }
    }
  }

  // Internal function to distribute Stele token bonus
  function distributeSteleBonus(uint256 challengeId, address recipient, uint256 amount, string memory action) internal {    
    IERC20Minimal steleTokenContract = IERC20Minimal(steleToken);
    uint256 contractBalance = steleTokenContract.balanceOf(address(this));
    
    if (contractBalance >= amount) {
      bool success = steleTokenContract.transfer(recipient, amount);
      if (success) {
        emit SteleTokenBonus(challengeId, recipient, action, amount);
      }
    }
    // Silently fail if insufficient balance - no revert to avoid breaking main functionality
  }

  // Mint Performance NFT for top 5 users after getRewards execution
  function mintPerformanceNFT(uint256 challengeId) external override {
    require(performanceNFTContract != address(0), "NNC"); // NFT contract Not set
    Challenge storage challenge = challenges[challengeId];
    require(challenge.startTime > 0, "CNE"); // Challenge Not Exists
    require(block.timestamp >= challenge.endTime, "NE"); // Not Ended
    
    // Check if caller is in top 5
    uint8 userRank = 0;
    bool isTopRanker = false;
    
    for (uint8 i = 0; i < 5; i++) {
      if (challenge.topUsers[i] == msg.sender) {
        userRank = i + 1; // rank starts from 1
        isTopRanker = true;
        break;
      }
    }
    
    require(isTopRanker, "NT5"); // Not Top 5
    
    // Check if user can mint NFT (haven't claimed yet)
    require(IStelePerformanceNFT(performanceNFTContract).canMintNFT(challengeId, msg.sender), "AC");
    
    // Get user's final scores
    uint256 finalScore = challenge.scores[userRank - 1];
    
    // Call NFT contract to mint
    IStelePerformanceNFT(performanceNFTContract).mintPerformanceNFT(
      challengeId,
      msg.sender,
      challenge.totalUsers,
      finalScore,
      userRank,
      challenge.seedMoney, // initial value
      challenge.challengeType,
      challenge.startTime
    );
  }
}
"
    },
    "contracts/libraries/PriceOracle.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

// Direct Uniswap V3 interfaces without library imports
interface IUniswapV3Factory {
    function getPool(address tokenA, address tokenB, uint24 fee) 
        external view returns (address pool);
}

interface IUniswapV3Pool {
    function observe(uint32[] calldata secondsAgos)
        external
        view
        returns (
            int56[] memory tickCumulatives, 
            uint160[] memory secondsPerLiquidityCumulativeX128s
        );
    
    function slot0() external view returns (
        uint160 sqrtPriceX96,
        int24 tick,
        uint16 observationIndex,
        uint16 observationCardinality,
        uint16 observationCardinalityNext,
        uint8 feeProtocol,
        bool unlocked
    );
}

library PriceOracle {
    
    // Get USD price from ETH (1 ETH = ? USD)
    function getETHPriceUSD(address uniswapV3Factory, address weth9, address usdToken) 
        internal view returns (uint256) {
        uint16[3] memory fees = [500, 3000, 10000];
        uint256 quoteAmount = 0;

        for (uint256 i=0; i<fees.length; i++) {
            address pool = IUniswapV3Factory(uniswapV3Factory).getPool(weth9, usdToken, uint24(fees[i]));
            if (pool == address(0)) {
                continue;
            }

            uint256 _quoteAmount = getQuoteFromPool(pool, uint128(1 * 10**18), weth9, usdToken);
            if (_quoteAmount > 0 && quoteAmount < _quoteAmount) {
                quoteAmount = _quoteAmount;
            }
        }

        return quoteAmount > 0 ? quoteAmount : 3000 * 1e6; // Fallback to $3000 if no pool available
    }

    // Get token price in ETH
    function getTokenPriceETH(address uniswapV3Factory, address baseToken, address weth9, uint256 baseAmount) 
        internal view returns (uint256) { 
        if (baseToken == weth9) {
            return baseAmount; // 1:1 ratio for WETH to ETH
        }

        uint16[3] memory fees = [500, 3000, 10000];
        uint256 quoteAmount = 0;

        for (uint256 i=0; i<fees.length; i++) {
            address pool = IUniswapV3Factory(uniswapV3Factory).getPool(baseToken, weth9, uint24(fees[i]));
            if (pool == address(0)) {
                continue;
            }

            uint256 _quoteAmount = getQuoteFromPool(pool, uint128(baseAmount), baseToken, weth9);
            if (_quoteAmount > 0 && quoteAmount < _quoteAmount) {
                quoteAmount = _quoteAmount;
            }
        }

        return quoteAmount;
    }

    // TWAP calculation using direct interface calls
    function getTWAPTick(address pool, uint32 secondsAgo) internal view returns (int24 timeWeightedAverageTick) {
        if (secondsAgo == 0) {
            (, timeWeightedAverageTick, , , , , ) = IUniswapV3Pool(pool).slot0();
            return timeWeightedAverageTick;
        }

        uint32[] memory secondsAgos = new uint32[](2);
        secondsAgos[0] = secondsAgo;
        secondsAgos[1] = 0;

        (int56[] memory tickCumulatives, ) = IUniswapV3Pool(pool).observe(secondsAgos);
        
        int56 tickCumulativesDelta = tickCumulatives[1] - tickCumulatives[0];
        timeWeightedAverageTick = int24(tickCumulativesDelta / int56(uint56(secondsAgo)));

        // Always round to negative infinity
        if (tickCumulativesDelta < 0 && (tickCumulativesDelta % int56(uint56(secondsAgo)) != 0)) {
            timeWeightedAverageTick--;
        }
    }

    // Convert tick to price ratio
    function getQuoteAtTick(int24 tick, uint128 baseAmount, address baseToken, address quoteToken) 
        internal pure returns (uint256 quoteAmount) {
        uint160 sqrtRatioX96 = getSqrtRatioAtTick(tick);
        
        // Calculate the price ratio from sqrtRatioX96
        if (sqrtRatioX96 <= type(uint128).max) {
            uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96;
            quoteAmount = baseToken < quoteToken
                ? mulDiv(ratioX192, baseAmount, 1 << 192)
                : mulDiv(1 << 192, baseAmount, ratioX192);
        } else {
            uint256 ratioX128 = mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64);
            quoteAmount = baseToken < quoteToken
                ? mulDiv(ratioX128, baseAmount, 1 << 128)
                : mulDiv(1 << 128, baseAmount, ratioX128);
        }
    }

    // Get sqrt ratio at tick (simplified version)
    function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) {
        uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick));
        require(absTick <= uint256(int256(887272)), 'T');

        uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000;
        if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128;
        if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128;
        if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128;
        if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128;
        if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128;
        if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128;
        if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128;
        if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128;
        if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128;
        if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128;
        if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128;
        if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128;
        if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128;
        if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128;
        if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128;
        if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128;
        if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128;
        if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128;
        if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128;

        if (tick > 0) ratio = type(uint256).max / ratio;

        sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1));
    }

    // Full precision multiplication
    function mulDiv(uint256 a, uint256 b, uint256 denominator) internal pure returns (uint256 result) {
        uint256 prod0;
        uint256 prod1;
        assembly {
            let mm := mulmod(a, b, not(0))
            prod0 := mul(a, b)
            prod1 := sub(sub(mm, prod0), lt(mm, prod0))
        }

        if (prod1 == 0) {
            require(denominator > 0);
            assembly {
                result := div(prod0, denominator)
            }
            return result;
        }

        require(denominator > prod1);

        uint256 remainder;
        assembly {
            remainder := mulmod(a, b, denominator)
        }
        assembly {
            prod1 := sub(prod1, gt(remainder, prod0))
            prod0 := sub(prod0, remainder)
        }

        uint256 twos = (~denominator + 1) & denominator;
        assembly {
            denominator := div(denominator, twos)
        }

        assembly {
            prod0 := div(prod0, twos)
        }
        assembly {
            twos := add(div(sub(0, twos), twos), 1)
        }
        prod0 |= prod1 * twos;

        uint256 inv = (3 * denominator) ^ 2;
        inv *= 2 - denominator * inv;
        inv *= 2 - denominator * inv;
        inv *= 2 - denominator * inv;
        inv *= 2 - denominator * inv;
        inv *= 2 - denominator * inv;
        inv *= 2 - denominator * inv;

        result = prod0 * inv;
        return result;
    }

    // Function to get quote from pool (may revert)
    function getQuoteFromPool(address pool, uint128 baseAmount, address baseToken, address quoteToken) 
        internal view returns (uint256) {
        uint32 secondsAgo = 1800; // 30 minutes TWAP
        int24 tick = getTWAPTick(pool, secondsAgo);
        return getQuoteAtTick(tick, baseAmount, baseToken, quoteToken);
    }

    // Precision multiplication helper function
    function precisionMul(uint256 x, uint256 y, uint256 precision) internal pure returns (uint256) {
        return (x * y) / precision;
    }
}"
    },
    "contracts/interfaces/IStele.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

interface IStele {
  // Enums
  enum ChallengeType { OneWeek, OneMonth, ThreeMonths, SixMonths, OneYear }
  
  // Events
  event SteleCreated(address owner, address usdToken, uint8 maxTokens, uint256 seedMoney, uint256 entryFee, uint256[5] rewardRatio);
  event RewardRatio(uint256[5] newRewardRatio);
  event EntryFee(uint256 newEntryFee);
  event MaxTokens(uint8 newMaxTokens);
  event SeedMoney(uint256 newSeedMoney);
  event AddToken(address tokenAddress);
  event RemoveToken(address tokenAddress);
  event Create(uint256 challengeId, ChallengeType challengeType, uint256 seedMoney, uint256 entryFee);
  event Join(uint256 challengeId, address user, uint256 seedMoney);
  event Swap(uint256 challengeId, address user, address tokenIn, address tokenOut, uint256 tokenInAmount, uint256 tokenOutAmount);
  event Register(uint256 challengeId, address user, uint256 performance);
  event Reward(uint256 challengeId, address user, uint256 rewardAmount);
  event SteleTokenBonus(uint256 challengeId, address indexed user, string action, uint256 amount);
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
  event PerformanceNFTContractSet(address indexed nftContract);
  event CreateBonusUpdated(uint256 newBonus);
  event JoinBonusUpdated(uint256 newBonus);
  event GetRewardsBonusUpdated(uint256 newBonus);

  // Read functions
  function owner() external view returns (address);
  function weth9() external view returns (address);
  function usdToken() external view returns (address);
  function usdTokenDecimals() external view returns (uint8);
  function maxTokens() external view returns (uint8);
  function seedMoney() external view returns (uint256);
  function entryFee() external view returns (uint256);
  function rewardRatio(uint256 index) external view returns (uint256);
  function isInvestable(address tokenAddress) external view returns (bool);
  function steleToken() external view returns (address);
  function createBonus() external view returns (uint256);
  function joinBonus() external view returns (uint256);
  function getRewardsBonus() external view returns (uint256);
  function performanceNFTContract() external view returns (address);
  function rewardsDistributed(uint256 challengeId) external view returns (bool);
  function getChallengeInfo(uint256 challengeId) external view returns (
    uint256 _id,
    ChallengeType _challengeType,
    uint256 _startTime,
    uint256 _endTime,
    uint256 _totalRewards,
    uint256 _seedMoney,
    uint256 _entryFee,
    uint32 _totalUsers
  );
  function challengeCounter() external view returns (uint256);
  function latestChallengesByType(ChallengeType challengeType) external view returns (uint256);
  
  // Governance functions (onlyOwner)
  function setRewardRatio(uint256[5] calldata _rewardRatio) external;
  function setEntryFee(uint256 _entryFee) external;
  function setSeedMoney(uint256 _seedMoney) external;
  function setToken(address tokenAddress) external;
  function setMaxTokens(uint8 _maxTokens) external;
  function resetToken(address tokenAddress) external;
  function transferOwnership(address newOwner) external;
  function setPerformanceNFTContract(address _nftContract) external;
  function setCreateBonus(uint256 _createBonus) external;
  function setJoinBonus(uint256 _joinBonus) external;
  function setGetRewardsBonus(uint256 _getRewardsBonus) external;

  // Challenge management functions
  function createChallenge(ChallengeType challengeType) external;
  function joinChallenge(uint256 challengeId) external;
  function swap(uint256 challengeId, address tokenIn, address tokenOut, uint256 tokenInAmount) external;
  function register(uint256 challengeId) external;
  // Reward function (onlyOwner)
  function getRewards(uint256 challengeId) external;

  function getUserPortfolio(uint256 challengeId, address user) external view returns (address[] memory tokenAddresses, uint256[] memory amounts);

  // Ranking function
  function getRanking(uint256 challengeId) external view returns (address[5] memory topUsers, uint256[5] memory scores);
  
  function mintPerformanceNFT(uint256 challengeId) external;
} "
    },
    "contracts/interfaces/IERC20Minimal.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

interface IERC20Minimal {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(
        address sender,
        address recipient,
        uint256 amount
    ) external returns (bool);
    function decimals() external view returns (uint8);
}"
    }
  }
}}

Tags:
ERC20, DeFi, Swap, Factory|addr:0xcc128f9ed90d5c584625d686c1a5d803f65d04ea|verified:true|block:23425238|tx:0xcfe2e9a5a1b7fb0e1c2cd567b0369c4ca50037be23343e8070d2c3fcce33f9cb|first_check:1758722812

Submitted on: 2025-09-24 16:06:57

Comments

Log in to comment.

No comments yet.