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