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);
}"
}
}
}}
Submitted on: 2025-10-31 13:06:16
Comments
Log in to comment.
No comments yet.