Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"settings": {
"optimizer": {
"enabled": true,
"runs": 600
},
"viaIR": true,
"outputSelection": {
"*": {
"*": [
"evm.bytecode",
"evm.deployedBytecode",
"devdoc",
"userdoc",
"metadata",
"abi"
]
}
},
"remappings": []
},
"sources": {
"contracts/SteleFund.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
// Simplified interfaces for Stele integration
import "./interfaces/ISteleFund.sol";
import "./interfaces/ISteleFundInfo.sol";
import "./interfaces/ISteleFundSetting.sol";
import "./interfaces/ISteleFundManagerNFT.sol";
import "./libraries/PriceOracle.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
interface IWETH9 {
function deposit() external payable;
function withdraw(uint256 wad) external;
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
interface IERC20Minimal {
function totalSupply() external view returns (uint256);
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);
}
contract SteleFund is ISteleFund, ReentrancyGuard {
using PriceOracle for address;
address public override owner;
// Uniswap V3 Contract
address public constant swapRouter = 0xE592427A0AEce92De3Edee1F18E0157C05861564;
address public constant uniswapV3Factory = 0x1F98431c8aD98523631AE4a59f267346ea31F984; // For price oracle
// Precision scaling for more accurate calculations
uint256 private constant BASIS_POINTS = 10000; // 100% = 10000 basis points
// Minimum thresholds to prevent dust issues
uint256 private constant MIN_DEPOSIT_USD = 10; // Minimum $10 deposit
// Maximum fund ID to prevent abuse
uint256 private constant MAX_FUND_ID = 1000000000; // Maximum 1 billion funds
// Maximum swaps per transaction to prevent DoS
uint256 private constant MAX_SWAPS_PER_TX = 10;
address public weth9;
address public setting;
address public info;
address public usdToken; // USDC address for price calculation
address public managerNFTContract; // SteleFundManagerNFT contract address
modifier onlyOwner() {
require(msg.sender == owner, 'NO');
_;
}
modifier onlyManager(address sender, uint256 fundId) {
require(fundId == ISteleFundInfo(info).managingFund(sender), "NM");
_;
}
constructor(
address _weth9,
address _setting,
address _info,
address _usdToken
) {
weth9 = _weth9;
setting = _setting;
info = _info;
usdToken = _usdToken;
owner = msg.sender;
}
// Safe fund ID parsing from calldata
function parseFundId(bytes memory data) private pure returns (uint256 fundId) {
require(data.length == 32, "IDL"); // Must be exactly 32 bytes for uint256
// Use standard ABI decoding for safety
fundId = abi.decode(data, (uint256));
// Prevent unreasonably large fund IDs
require(fundId > 0 && fundId <= MAX_FUND_ID, "FID");
}
// Calculate portfolio total value in USD
function getPortfolioValueUSD(uint256 fundId) internal view returns (uint256) {
IToken.Token[] memory fundTokens = ISteleFundInfo(info).getFundTokens(fundId);
uint256 totalValueUSD = 0;
for (uint256 i = 0; i < fundTokens.length; i++) {
if (fundTokens[i].amount > 0) {
uint256 tokenValueUSD = PriceOracle.getTokenPriceUSD(uniswapV3Factory, fundTokens[i].token, fundTokens[i].amount, weth9, usdToken);
totalValueUSD += tokenValueUSD;
}
}
return totalValueUSD;
}
// Calculate shares to mint based on USD value with improved precision
function _calculateSharesToMint(uint256 fundId, address token, uint256 amount) private view returns (uint256) {
uint256 fundShare = ISteleFundInfo(info).getFundShare(fundId);
// First deposit: shares = USD value of deposit
if (fundShare == 0) {
uint256 usdValue = PriceOracle.getTokenPriceUSD(uniswapV3Factory, token, amount, weth9, usdToken);
return usdValue;
}
// Get deposit value in USD
uint256 depositValueUSD = PriceOracle.getTokenPriceUSD(uniswapV3Factory, token, amount, weth9, usdToken);
if (depositValueUSD == 0) return 0;
// Get current portfolio value in USD
uint256 portfolioValueUSD = getPortfolioValueUSD(fundId);
if (portfolioValueUSD == 0) {
return depositValueUSD;
}
// Use mulDiv for maximum precision: (depositValue * existingShares) / portfolioValue
// This avoids intermediate overflow and maintains precision
uint256 shares = PriceOracle.mulDiv(depositValueUSD, fundShare, portfolioValueUSD);
// Round up to favor the protocol (prevent rounding attacks)
if ((depositValueUSD * fundShare) % portfolioValueUSD > 0) {
shares += 1;
}
return shares;
}
fallback() external payable nonReentrant {
// Safe fund ID parsing with validation
uint256 fundId = parseFundId(msg.data);
// Verify fund exists (fundId > 0 already checked in parseFundId)
require(fundId <= ISteleFundInfo(info).fundIdCount(), "FNE");
require(ISteleFundInfo(info).isJoined(msg.sender, fundId), "US");
// Check minimum USD deposit amount
{
uint256 depositUSD = PriceOracle.getTokenPriceUSD(uniswapV3Factory, weth9, msg.value, weth9, usdToken);
uint8 decimals = IERC20Minimal(usdToken).decimals();
require(decimals <= 18, "ID"); // Prevent overflow
require(depositUSD >= MIN_DEPOSIT_USD * (10 ** uint256(decimals)), "MDA"); // Minimum $10 deposit
}
// Calculate manager fee (only for investors, not manager)
uint256 feeAmount = 0;
uint256 fundAmount = msg.value;
if (msg.sender != ISteleFundInfo(info).manager(fundId)) {
uint256 feeRate = ISteleFundSetting(setting).managerFee();
feeAmount = PriceOracle.mulDiv(msg.value, feeRate, BASIS_POINTS);
fundAmount = msg.value - feeAmount;
}
// Calculate shares based on net deposit amount (after fee deduction)
uint256 sharesToMint = _calculateSharesToMint(fundId, weth9, fundAmount);
require(sharesToMint > 0, "ZS"); // Zero shares
// Update state FIRST (before external calls)
ISteleFundInfo(info).increaseFundToken(fundId, weth9, fundAmount); // Net amount to fund pool
if (feeAmount > 0) {
ISteleFundInfo(info).increaseFeeToken(fundId, weth9, feeAmount); // Fee amount to fee pool
}
(uint256 investorShare, uint256 fundShare) = ISteleFundInfo(info).increaseShare(fundId, msg.sender, sharesToMint);
emit Deposit(fundId, msg.sender, weth9, msg.value, investorShare, fundShare, fundAmount, feeAmount);
// External call LAST
IWETH9(weth9).deposit{value: msg.value}();
}
receive() external payable {
if (msg.sender == weth9) {
// when call IWETH9(weth9).withdraw(amount) in this contract
} else {
// when deposit ETH with no data
}
}
function withdraw(uint256 fundId, uint256 percentage) external payable override nonReentrant {
bool isJoined = ISteleFundInfo(info).isJoined(msg.sender, fundId);
require(isJoined, "US");
require(percentage > 0 && percentage <= 10000, "IP"); // 0.01% to 100%
uint256 investorShare = ISteleFundInfo(info).getInvestorShare(fundId, msg.sender);
require(investorShare > 0, "NS");
_withdraw(fundId, investorShare, percentage);
}
function _withdraw(uint256 fundId, uint256 investorShareBefore, uint256 percentage) private {
IToken.Token[] memory fundTokens = ISteleFundInfo(info).getFundTokens(fundId);
uint256 fundShare = ISteleFundInfo(info).getFundShare(fundId);
require(fundShare > 0, "ZFS"); // Zero fund shares
uint256 shareToWithdraw = PriceOracle.mulDiv(investorShareBefore, percentage, 10000);
// If shareToWithdraw is 0 due to rounding, just return - no need to complicate
if (shareToWithdraw == 0) {
return; // No withdrawal, save gas
}
// Update state FIRST (before external calls)
(uint256 investorShareAfter, uint256 fundShareAfter) = ISteleFundInfo(info).decreaseShare(fundId, msg.sender, shareToWithdraw);
emit Withdraw(fundId, msg.sender, percentage, investorShareAfter, fundShareAfter);
for (uint256 i = 0; i < fundTokens.length; i++) {
if (fundTokens[i].amount > 0) {
// Calculate token amount with overflow protection using mulDiv
// Calculate token share directly: (amount * investorShareBefore * percentage) / (fundShare * 10000)
uint256 tokenShare = PriceOracle.mulDiv(
PriceOracle.mulDiv(fundTokens[i].amount, investorShareBefore, fundShare),
percentage,
10000
);
// Ensure we don't withdraw more than available
if (tokenShare > fundTokens[i].amount) {
tokenShare = fundTokens[i].amount;
}
if (tokenShare > 0) {
address token = fundTokens[i].token;
// Update state FIRST (before external calls)
ISteleFundInfo(info).decreaseFundToken(fundId, token, tokenShare);
// External calls LAST
if (token == weth9) {
IWETH9(weth9).withdraw(tokenShare);
(bool success, ) = payable(msg.sender).call{value: tokenShare}("");
require(success, "FW");
} else {
IERC20Minimal(token).transfer(msg.sender, tokenShare);
}
}
}
}
}
// Uniswap V3 Swap Implementation with slippage protection
function executeV3Swap(uint256 fundId, SwapParams calldata trade) private {
require(ISteleFundSetting(setting).isInvestable(trade.tokenOut), "NWT");
require(trade.amountIn <= ISteleFundInfo(info).getFundTokenAmount(fundId, trade.tokenIn), "NET");
// Validate slippage and check token limits
_validateSwapParameters(fundId, trade);
// Calculate effective minimum output
uint256 effectiveMinOutput = _calculateEffectiveMinOutput(trade);
// Update state FIRST - decrease the token we're swapping from
ISteleFundInfo(info).decreaseFundToken(fundId, trade.tokenIn, trade.amountIn);
// Execute the swap
uint256 amountOut = _executeSwapCall(trade, effectiveMinOutput);
// Validate output and update state
require(amountOut >= effectiveMinOutput, "SLP");
ISteleFundInfo(info).increaseFundToken(fundId, trade.tokenOut, amountOut);
emit Swap(fundId, trade.tokenIn, trade.tokenOut, trade.amountIn, amountOut);
}
// Helper function to validate swap parameters
function _validateSwapParameters(uint256 fundId, SwapParams calldata trade) private view {
// Check maxTokens limit for new tokens
if (ISteleFundInfo(info).getFundTokenAmount(fundId, trade.tokenOut) == 0) {
IToken.Token[] memory fundTokens = ISteleFundInfo(info).getFundTokens(fundId);
uint256 currentTokenTypes = 0;
for (uint256 i = 0; i < fundTokens.length; i++) {
if (fundTokens[i].amount > 0) {
currentTokenTypes++;
}
}
require(currentTokenTypes < ISteleFundSetting(setting).maxTokens(), "MAX");
}
}
// Helper function to calculate effective minimum output
function _calculateEffectiveMinOutput(SwapParams calldata trade) private view returns (uint256) {
uint256 expectedOutput = PriceOracle.getBestQuote(
uniswapV3Factory,
trade.tokenIn,
trade.tokenOut,
uint128(trade.amountIn),
300
);
uint256 slippage = ISteleFundSetting(setting).maxSlippage();
uint256 minOutputWithSlippage = PriceOracle.mulDiv(expectedOutput, BASIS_POINTS - slippage, BASIS_POINTS);
require(trade.amountOutMinimum >= minOutputWithSlippage, "ESP"); // Excessive slippage protection
return trade.amountOutMinimum;
}
// Helper function to execute swap call
function _executeSwapCall(SwapParams calldata trade, uint256 effectiveMinOutput) private returns (uint256) {
// Safe approve pattern: reset to 0 first, then set new amount
// This prevents issues with tokens like USDT that don't allow changing non-zero allowances
IERC20Minimal(trade.tokenIn).approve(swapRouter, 0);
IERC20Minimal(trade.tokenIn).approve(swapRouter, trade.amountIn);
bytes memory swapCall = abi.encodeWithSignature(
"exactInputSingle((address,address,uint24,address,uint256,uint256,uint256,uint160))",
trade.tokenIn,
trade.tokenOut,
trade.fee,
address(this),
block.timestamp + 300,
trade.amountIn,
effectiveMinOutput,
0
);
uint256 balanceBefore = IERC20Minimal(trade.tokenOut).balanceOf(address(this));
(bool success, ) = swapRouter.call(swapCall);
require(success, "SWF");
return IERC20Minimal(trade.tokenOut).balanceOf(address(this)) - balanceBefore;
}
function swap(uint256 fundId, SwapParams[] calldata trades)
external override onlyManager(msg.sender, fundId) nonReentrant
{
require(trades.length <= MAX_SWAPS_PER_TX, "TMS"); // Too Many Swaps
for(uint256 i=0; i<trades.length; i++)
{
// Use Uniswap V3 SwapRouter for all swaps
executeV3Swap(fundId, trades[i]);
}
}
function withdrawFee(uint256 fundId, address token, uint256 percentage)
external payable override onlyManager(msg.sender, fundId) nonReentrant
{
require(percentage > 0 && percentage <= 10000, "IP"); // 0.01% to 100%
uint256 totalFeeAmount = ISteleFundInfo(info).getFeeTokenAmount(fundId, token);
require(totalFeeAmount > 0, "NF"); // No fee available
// Calculate amount to withdraw using high precision
uint256 amount = PriceOracle.precisionMul(totalFeeAmount, percentage, 10000);
// If amount is 0 due to rounding, return
if (amount == 0) {
return; // Save gas
}
// Ensure we don't withdraw more than available
if (amount > totalFeeAmount) {
amount = totalFeeAmount;
}
// Update state FIRST (before external calls)
bool isSuccess = ISteleFundInfo(info).decreaseFeeToken(fundId, token, amount);
require(isSuccess, "FD");
// This line should not be here - fee tokens are separate from fund tokens
// ISteleFundInfo(info).decreaseFundToken(fundId, token, amount);
emit WithdrawFee(fundId, msg.sender, token, amount);
// External calls LAST
if (token == weth9) {
IWETH9(weth9).withdraw(amount);
(bool success, ) = payable(msg.sender).call{value: amount}("");
require(success, "FW");
} else {
IERC20Minimal(token).transfer(msg.sender, amount);
}
}
// Transfer ownership (only owner)
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "ZA"); // Zero Address
owner = newOwner;
emit OwnershipTransferred(msg.sender, newOwner);
}
// Set Manager NFT Contract (only callable by info contract owner)
function setManagerNFTContract(address _managerNFTContract) external override onlyOwner {
require(_managerNFTContract != address(0), "NZ");
managerNFTContract = _managerNFTContract;
emit ManagerNFTContractSet(_managerNFTContract);
}
// Get Manager NFT Contract address
function getManagerNFTContract() external view override returns (address) {
return managerNFTContract;
}
// Mint Manager NFT (only callable by fund manager)
function mintManagerNFT(uint256 fundId) external override onlyManager(msg.sender, fundId) nonReentrant returns (uint256) {
require(managerNFTContract != address(0), "NNC"); // NFT Contract Not set
address manager = ISteleFundInfo(info).manager(fundId);
require(manager == msg.sender, "NM");
// Create mint parameters
MintParams memory params = MintParams({
fundId: fundId,
fundCreated: ISteleFundInfo(info).fundCreationBlock(fundId), // Get actual fund creation block
investment: ISteleFundInfo(info).getFundShare(fundId),
currentTVL: getPortfolioValueUSD(fundId)
});
// Call NFT contract to mint
return ISteleFundManagerNFT(managerNFTContract).mintManagerNFT(params);
}
}
"
},
"@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: MIT
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 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
);
}
/// @title Price Oracle Library
/// @notice Library for calculating Time-Weighted Average Prices using Uniswap V3
/// @dev Provides functions for TWAP 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%
}
// Default TWAP period (30 minutes)
uint32 public constant DEFAULT_TWAP_PERIOD = 1800;
/// @notice Calculate TWAP tick for a given pool and time period
/// @param pool The Uniswap V3 pool address
/// @param secondsAgo The time period for TWAP calculation
/// @return timeWeightedAverageTick The calculated TWAP tick
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--;
}
}
/// @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 TWAP
/// @param pool The pool address
/// @param baseAmount The base amount
/// @param baseToken The base token address
/// @param quoteToken The quote token address
/// @param secondsAgo TWAP period in seconds
/// @return quoteAmount The calculated quote amount
function getQuoteFromPool(
address pool,
uint128 baseAmount,
address baseToken,
address quoteToken,
uint32 secondsAgo
) internal view returns (uint256 quoteAmount) {
int24 tick = getTWAPTick(pool, secondsAgo);
return getQuoteAtTick(tick, baseAmount, baseToken, quoteToken);
}
/// @notice Get best quote across multiple fee tiers
/// @param factory The Uniswap V3 factory address
/// @param tokenA First token address
/// @param tokenB Second token address
/// @param amountIn Input amount
/// @param secondsAgo TWAP period in seconds
/// @return bestQuote The best quote found across all pools
function getBestQuote(
address factory,
address tokenA,
address tokenB,
uint128 amountIn,
uint32 secondsAgo
) 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, secondsAgo);
if (quote > bestQuote) {
bestQuote = quote;
}
}
}
/// @notice Get ETH price in USD using TWAP
/// @param factory The Uniswap V3 factory address
/// @param weth9 WETH9 token address
/// @param usdToken USD token address (e.g., USDC)
/// @param fallbackPrice Fallback price if no pools available
/// @return ethPriceUSD ETH price in USD
function getETHPriceUSD(
address factory,
address weth9,
address usdToken,
uint256 fallbackPrice
) internal view returns (uint256 ethPriceUSD) {
uint256 quote = getBestQuote(
factory,
weth9,
usdToken,
uint128(1e18), // 1 ETH
DEFAULT_TWAP_PERIOD
);
return quote > 0 ? quote : fallbackPrice;
}
/// @notice Get token price in ETH using TWAP
/// @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),
DEFAULT_TWAP_PERIOD
);
}
/// @notice Get token price in USD using TWAP
/// @param factory The Uniswap V3 factory address
/// @param token Token address
/// @param amount Token amount
/// @param weth9 WETH9 token address
/// @param usdToken USD token address (e.g., USDC)
/// @return usdAmount USD amount equivalent
function getTokenPriceUSD(
address factory,
address token,
uint256 amount,
address weth9,
address usdToken
) internal view returns (uint256 usdAmount) {
if (token == weth9) {
// ETH to USD directly
uint256 ethPriceUSD = getETHPriceUSD(factory, weth9, usdToken, 3000 * 1e6);
return precisionMul(amount, ethPriceUSD, 1e18);
} else if (token == usdToken) {
// USD token (USDC) - return as is
return amount;
} else {
// Other tokens: token -> ETH -> USD
uint256 ethAmount = getTokenPriceETH(factory, token, weth9, amount);
if (ethAmount == 0) return 0;
uint256 ethPriceUSD = getETHPriceUSD(factory, weth9, usdToken, 3000 * 1e6);
return precisionMul(ethAmount, ethPriceUSD, 1e18);
}
}
/// @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/ISteleFundManagerNFT.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
// Mint parameters structure to avoid stack too deep
struct MintParams {
uint256 fundId;
uint256 fundCreated;
uint256 investment;
uint256 currentTVL;
}
interface ISteleFundManagerNFT {
// Events
event ManagerNFTMinted(
uint256 indexed tokenId,
uint256 indexed fundId,
address indexed manager,
uint256 investment,
uint256 currentTVL,
int256 returnRate,
uint256 fundCreated
);
event TransferAttemptBlocked(uint256 indexed tokenId, address indexed from, address indexed to, string reason);
// Main functions
function mintManagerNFT(MintParams calldata params) external returns (uint256);
// View functions
function getTokenData(uint256 tokenId) external view returns (
uint256 fundId,
uint256 fundCreated,
uint256 nftMintBlock,
uint256 investment,
uint256 currentTVL,
int256 returnRate
);
}"
},
"contracts/interfaces/ISteleFundSetting.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
interface ISteleFundSetting {
event SettingCreated();
event OwnerChanged(address oldOwner, address newOwner);
event ManagerFeeChanged(uint256 managerFee);
event AddToken(address indexed token);
event RemoveToken(address indexed token);
event MaxTokensChanged(uint256 maxTokens);
event MaxSlippageChanged(uint256 maxSlippage);
function owner() external view returns (address);
function weth9() external view returns (address);
function usdc() external view returns (address);
function managerFee() external view returns (uint256);
function maxTokens() external view returns (uint256);
function maxSlippage() external view returns (uint256);
function isInvestable(address _token) external view returns (bool);
function setOwner(address _owner) external;
function setManagerFee(uint256 _managerFee) external;
function setMaxTokens(uint256 _maxTokens) external;
function setMaxSlippage(uint256 _maxSlippage) external;
function setToken(address _token) external;
function resetToken(address _token) external;
}"
},
"contracts/interfaces/ISteleFundInfo.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import './IToken.sol';
interface ISteleFundInfo is IToken {
event InfoCreated();
event OwnerChanged(address owner, address newOwner);
event Create(uint256 fundId, address indexed manager);
event Join(uint256 fundId, address indexed investor);
function owner() external view returns (address _owner);
function manager(uint256 fundId) external view returns (address _manager);
function managingFund(address _manager) external view returns (uint256 fundId);
function fundIdCount() external view returns (uint256 fundCount);
function fundCreationBlock(uint256 fundId) external view returns (uint256 creationBlock);
function setOwner(address newOwner) external;
function create() external returns (uint256 fundId);
function isJoined(address investor, uint256 fundId) external view returns (bool);
function join(uint256 fundId) external;
function getFundTokens(uint256 fundId) external view returns (Token[] memory);
function getFeeTokens(uint256 fundId) external view returns (Token[] memory);
function getFundTokenAmount(uint256 fundId, address token) external view returns (uint256);
function getFeeTokenAmount(uint256 fundId, address token) external view returns (uint256);
function getFundShare(uint256 fundId) external view returns (uint256);
function getInvestorShare(uint256 fundId, address investor) external view returns (uint256);
function increaseFundToken(uint256 fundId, address token, uint256 amount) external;
function decreaseFundToken(uint256 fundId, address token, uint256 amount) external returns (bool);
function increaseShare(uint256 fundId, address investor, uint256 amount) external returns (uint256, uint256);
function decreaseShare(uint256 fundId, address investor, uint256 amount) external returns (uint256, uint256);
function increaseFeeToken(uint256 fundId, address token, uint256 amount) external;
function decreaseFeeToken(uint256 fundId, address token, uint256 amount) external returns (bool);
}"
},
"contracts/interfaces/ISteleFund.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
interface ISteleFund {
event Deposit(uint256 fundId, address indexed investor, address token, uint256 amount, uint256 investorShare, uint256 fundShare, uint256 fundAmount, uint256 feeAmount);
event Withdraw(uint256 fundId, address indexed investor, uint256 percentage, uint256 investorShare, uint256 fundShare);
event Swap(uint256 fundId, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut);
event WithdrawFee(uint256 fundId, address indexed manager, address token, uint256 amount);
event ManagerNFTContractSet(address indexed managerNFTContract);
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
struct SwapParams {
address tokenIn;
address tokenOut;
uint24 fee;
uint256 amountIn;
uint256 amountOutMinimum;
}
function owner() external view returns (address);
function withdraw(uint256 fundId, uint256 percentage) external payable;
function swap(uint256 fundId, SwapParams[] calldata trades) external;
function withdrawFee(uint256 fundId, address token, uint256 percentage) external payable;
function setManagerNFTContract(address _managerNFTContract) external;
function getManagerNFTContract() external view returns (address);
function mintManagerNFT(uint256 fundId) external returns (uint256);
}"
},
"contracts/interfaces/IToken.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
interface IToken {
struct Token {
address token;
uint256 amount;
}
}"
}
}
}}
Submitted on: 2025-09-24 15:18:24
Comments
Log in to comment.
No comments yet.