Stele

Description:

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

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

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

import './interfaces/IERC20Minimal.sol';
import './interfaces/IStele.sol';
import {PriceOracle, IUniswapV3Factory} from './libraries/PriceOracle.sol';
import "@openzeppelin/contracts/security/ReentrancyGuard.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, ReentrancyGuard {  
  address public constant uniswapV3Factory = 0x1F98431c8aD98523631AE4a59f267346ea31F984;
  
  // State variables
  address public override owner;

  address public override usdToken;
  address public override weth9;
  address public override wbtc;
  address public override uni;
  address public override link;

  uint256 public override seedMoney;
  uint256 public override entryFee;
  uint8 public override usdTokenDecimals;
  uint256[5] public override rewardRatio;
  mapping(address => bool) public override isInvestable;
  mapping(uint256 => bool) public override rewardsDistributed;

  // 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 _wbtc, address _uni, address _link) {
    owner = msg.sender;

    usdToken = _usdToken;
    weth9 = _weth9;
    wbtc = _wbtc;
    uni = _uni;
    link = _link;

    usdTokenDecimals = IERC20Minimal(_usdToken).decimals(); 
    seedMoney = 10000 * 10**usdTokenDecimals;
    entryFee = 5 * 10**usdTokenDecimals; // 5 USD
    rewardRatio = [50, 26, 13, 7, 4];
    challengeCounter = 0;

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

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

  // Duration in seconds for each challenge type
  function getDuration(IStele.ChallengeType challengeType) internal pure returns (uint256) {
    if (challengeType == IStele.ChallengeType.OneWeek) return 7 days;
    return 0;
  }

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

  // Join an existing challenge
  function joinChallenge(uint256 challengeId) external override nonReentrant {
    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);

    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 high precision using mulDiv
    // Step 1: Convert amount to USD value
    uint256 valueInUSD = PriceOracle.mulDiv(
      amount,
      tokenInPriceUSD,
      10 ** tokenInDecimals
    );

    // Step 2: Convert USD value to output token amount
    uint256 toAmount = PriceOracle.mulDiv(
      valueInUSD,
      10 ** tokenOutDecimals,
      tokenOutPriceUSD
    );

    // 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) {
      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");

    // Check if user has joined the challenge
    UserPortfolio memory portfolio = challenge.portfolios[msg.sender];
    require(portfolio.tokens.length > 0, "NJ"); // Not Joined

    // 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
    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];
    }
  }

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

  // Renounce ownership of the contract
  function renounceOwnership() external onlyOwner {
    emit OwnershipTransferred(owner, address(0));
    owner = address(0);
  }

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

  // Claim rewards after challenge ends
  function getRewards(uint256 challengeId) external override nonReentrant {
    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 in reverse order (rank 5 to rank 1)
      // This ensures rank 1 gets all remaining funds to avoid precision loss
      for (uint8 i = actualRankerCount; i > 0; i--) {
        uint8 idx = i - 1; // Convert to 0-indexed
        address userAddress = validRankers[idx];

        uint256 rewardAmount;

        // Give all remaining funds to the first ranker (rank 1) to avoid precision loss
        if (idx == 0) {
          rewardAmount = undistributed;
        } else {
          // Calculate reward based on original ratio
          require(totalInitialRewardWeight > 0, "IW");
          // Use direct calculation to avoid precision loss
          rewardAmount = (challenge.totalRewards * initialRewards[idx]) / 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);
        }
      }
    }
  }

  // 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
    );
  }
}
"
    },
    "@openzeppelin/contracts/security/ReentrancyGuard.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol)

pragma solidity ^0.8.0;

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;

    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be _NOT_ENTERED
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");

        // Any calls to nonReentrant after this point will fail
        _status = _ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = _NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == _ENTERED;
    }
}
"
    },
    "contracts/libraries/PriceOracle.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.28;

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

interface IUniswapV3Pool {
    function slot0() external view returns (
        uint160 sqrtPriceX96,
        int24 tick,
        uint16 observationIndex,
        uint16 observationCardinality,
        uint16 observationCardinalityNext,
        uint8 feeProtocol,
        bool unlocked
    );
}

/// @title Price Oracle Library
/// @notice Library for calculating Spot Prices using Uniswap V3
/// @dev Provides functions for spot price calculation, tick math, and price conversion
library PriceOracle {

    // Standard Uniswap V3 fee tiers - using function to return array
    function getFeeTiers() private pure returns (uint16[3] memory) {
        return [uint16(500), uint16(3000), uint16(10000)]; // 0.05%, 0.3%, 1%
    }

    /// @notice Convert tick to sqrt price ratio
    /// @param tick The tick value
    /// @return sqrtPriceX96 The sqrt price in X96 format
    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));
    }

    /// @notice Full precision multiplication
    /// @param a First number
    /// @param b Second number
    /// @param denominator Denominator for division
    /// @return result The result of (a * b) / denominator
    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;
    }

    /// @notice Convert tick to price quote
    /// @param tick The tick value
    /// @param baseAmount The base amount to convert
    /// @param baseToken The base token address
    /// @param quoteToken The quote token address
    /// @return quoteAmount The calculated quote amount
    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);
        }
    }

    /// @notice Get quote from pool using spot price
    /// @param pool The pool address
    /// @param baseAmount The base amount
    /// @param baseToken The base token address
    /// @param quoteToken The quote token address
    /// @return quoteAmount The calculated quote amount
    function getQuoteFromPool(
        address pool,
        uint128 baseAmount,
        address baseToken,
        address quoteToken
    ) internal view returns (uint256 quoteAmount) {
        // Use spot price (like Uniswap SwapRouter)
        (, int24 tick, , , , , ) = IUniswapV3Pool(pool).slot0();

        return getQuoteAtTick(tick, baseAmount, baseToken, quoteToken);
    }

    /// @notice Get best quote across multiple fee tiers using spot price
    /// @param factory The Uniswap V3 factory address
    /// @param tokenA First token address
    /// @param tokenB Second token address
    /// @param amountIn Input amount
    /// @return bestQuote The best quote found across all pools
    function getBestQuote(
        address factory,
        address tokenA,
        address tokenB,
        uint128 amountIn
    ) internal view returns (uint256 bestQuote) {
        bestQuote = 0;

        uint16[3] memory feeTiers = getFeeTiers();
        for (uint256 i = 0; i < feeTiers.length; i++) {
            address pool = IUniswapV3Factory(factory).getPool(tokenA, tokenB, uint24(feeTiers[i]));
            if (pool == address(0)) {
                continue;
            }

            // Note: Direct call without try-catch since we're in a library
            // The calling contract should handle exceptions
            uint256 quote = getQuoteFromPool(pool, amountIn, tokenA, tokenB);
            if (quote > bestQuote) {
                bestQuote = quote;
            }
        }
    }

    /// @notice Get ETH price in USD using spot price
    /// @dev Reverts if no valid price is available
    /// @param factory The Uniswap V3 factory address
    /// @param weth9 WETH9 token address
    /// @param usdToken USD token address (e.g., USDC)
    /// @return ethPriceUSD ETH price in USD
    function getETHPriceUSD(
        address factory,
        address weth9,
        address usdToken
    ) internal view returns (uint256 ethPriceUSD) {
        uint256 quote = getBestQuote(
            factory,
            weth9,
            usdToken,
            uint128(1e18) // 1 ETH
        );

        require(quote > 0, "No valid ETH price available");
        return quote;
    }

    /// @notice Get token price in ETH using spot price
    /// @param factory The Uniswap V3 factory address
    /// @param token Token address
    /// @param weth9 WETH9 token address
    /// @param amount Token amount
    /// @return ethAmount ETH amount equivalent
    function getTokenPriceETH(
        address factory,
        address token,
        address weth9,
        uint256 amount
    ) internal view returns (uint256 ethAmount) {
        if (token == weth9) {
            return amount; // 1:1 ratio for WETH to ETH
        }

        return getBestQuote(
            factory,
            token,
            weth9,
            uint128(amount)
        );
    }

    /// @notice High precision multiplication: (a * b) / c
    /// @param a First number
    /// @param b Second number
    /// @param c Divisor
    /// @return result The result of (a * b) / c with high precision
    function precisionMul(uint256 a, uint256 b, uint256 c) internal pure returns (uint256) {
        if (a == 0 || b == 0) return 0;
        require(c > 0, "Division by zero");

        uint256 PRECISION_SCALE = 1e18;

        // Check if we can safely multiply with precision scale
        if (a <= type(uint256).max / b && (a * b) <= type(uint256).max / PRECISION_SCALE) {
            return (a * b * PRECISION_SCALE) / (c * PRECISION_SCALE);
        }

        // Fallback to standard calculation to avoid overflow
        return (a * b) / c;
    }
}
"
    },
    "contracts/interfaces/IStele.sol": {
      "content": "// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.28;

interface IStele {
  // Enums
  enum ChallengeType { OneWeek }
  
  // Events
  event SteleCreated(address owner, address usdToken, uint256 seedMoney, uint256 entryFee, uint256[5] rewardRatio);
  event AddToken(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 OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
  event PerformanceNFTContractSet(address indexed nftContract);

  // Read functions
  function owner() external view returns (address);
  function weth9() external view returns (address);
  function usdToken() external view returns (address);
  function wbtc() external view returns (address);
  function uni() external view returns (address);
  function link() external view returns (address);
  function usdTokenDecimals() 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 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);
  
  function setPerformanceNFTContract(address _nftContract) external;
  function transferOwnership(address newOwner) external;
  function renounceOwnership() 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;
  function getRewards(uint256 challengeId) external;
  function mintPerformanceNFT(uint256 challengeId) external;

  function getRanking(uint256 challengeId) external view returns (address[5] memory topUsers, uint256[5] memory scores);
  function getUserPortfolio(uint256 challengeId, address user) external view returns (address[] memory tokenAddresses, uint256[] memory amounts);  
} "
    },
    "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, Oracle|addr:0x1de26fd50af9bea93ec4ec78747064c981c743cd|verified:true|block:23695967|tx:0x1f929c7b8873c23b1ce958d62dac9975b59f7ae0f4f20f618d4650c17b45e9f9|first_check:1761912376

Submitted on: 2025-10-31 13:06:16

Comments

Log in to comment.

No comments yet.