UniswapOracle

Description:

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

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "hopium/uniswap/main/uniswap-oracle.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/uniswap/interface/imPoolFinder.sol";
import "hopium/common/lib/full-math.sol";

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
}

interface IERC20Metadata is IERC20 {
    function decimals() external view returns (uint8);
}

interface IUniswapV2Pair {
    function token0() external view returns (address);
    function token1() external view returns (address);
    function getReserves()
        external
        view
        returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}

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


abstract contract Storage {
    address public immutable WETH_ADDRESS;
    address public immutable USD_ADDRESS;
    address public immutable WETH_USD_PAIR_ADDRESS;
    bool public immutable IS_WETH_USD_PAIR_ADDRESS_V3;

    uint8 public immutable WETH_DECIMALS;
}

abstract contract POW {
    uint256 constant WAD = 1e18;

    // Cheap powers of 10 (0..18). Using a lookup table is much cheaper than 10 ** x.
    uint256 private constant POW10_0  = 1;
    uint256 private constant POW10_1  = 10;
    uint256 private constant POW10_2  = 100;
    uint256 private constant POW10_3  = 1_000;
    uint256 private constant POW10_4  = 10_000;
    uint256 private constant POW10_5  = 100_000;
    uint256 private constant POW10_6  = 1_000_000;
    uint256 private constant POW10_7  = 10_000_000;
    uint256 private constant POW10_8  = 100_000_000;
    uint256 private constant POW10_9  = 1_000_000_000;
    uint256 private constant POW10_10 = 10_000_000_000;
    uint256 private constant POW10_11 = 100_000_000_000;
    uint256 private constant POW10_12 = 1_000_000_000_000;
    uint256 private constant POW10_13 = 10_000_000_000_000;
    uint256 private constant POW10_14 = 100_000_000_000_000;
    uint256 private constant POW10_15 = 1_000_000_000_000_000;
    uint256 private constant POW10_16 = 10_000_000_000_000_000;
    uint256 private constant POW10_17 = 100_000_000_000_000_000;
    uint256 private constant POW10_18 = 1_000_000_000_000_000_000;

    /// Fast 10^n for 0 <= n <= 18 using constants above.
    function _pow10(uint8 n) internal pure returns (uint256) {
        if (n == 0)  return POW10_0;
        if (n == 1)  return POW10_1;
        if (n == 2)  return POW10_2;
        if (n == 3)  return POW10_3;
        if (n == 4)  return POW10_4;
        if (n == 5)  return POW10_5;
        if (n == 6)  return POW10_6;
        if (n == 7)  return POW10_7;
        if (n == 8)  return POW10_8;
        if (n == 9)  return POW10_9;
        if (n == 10) return POW10_10;
        if (n == 11) return POW10_11;
        if (n == 12) return POW10_12;
        if (n == 13) return POW10_13;
        if (n == 14) return POW10_14;
        if (n == 15) return POW10_15;
        if (n == 16) return POW10_16;
        if (n == 17) return POW10_17;
        // n == 18
        return POW10_18;
    }

    error EmptyReserves();
    /// price18 = (reserveA * 1e18 * 10^(decB - decA)) / reserveB, computed with overflow-safe mulDiv.
    function _price18(
        uint256 reserveA,
        uint8 decA,
        uint256 reserveB,
        uint8 decB
    ) internal pure returns (uint256) {
        if (reserveA == 0 || reserveB == 0) revert EmptyReserves();
        // Scale factor = 1e18 * 10^(decB - decA)
        uint256 scale;
        unchecked {
            if (decB >= decA) {
                scale = WAD * _pow10(decB - decA);
            } else {
                // Implement as mulDiv(reserveA, WAD, reserveB * 10^(decA - decB))
                uint256 denom = reserveB * _pow10(decA - decB);
                return FullMath.mulDiv(reserveA, WAD, denom);
            }
        }
        return FullMath.mulDiv(reserveA, scale, reserveB);
    }

    /// Scale arbitrary decimals to 1e18.
    function _scaleTo1e18(uint256 amount, uint8 decimals) internal pure returns (uint256 scaled) {
        if (decimals == 18) return amount;
        unchecked {
            if (decimals < 18) {
                return amount * _pow10(18 - decimals);
            } else {
                return amount / _pow10(decimals - 18);
            }
        }
    }
}

abstract contract PoolFinder is ImPoolFinder {
    error PoolDoesNotExist();
    function _findTokenWethPoolAddress(address tokenAddress)
        internal
        view
        returns (bool isV3Pool, address poolAddress)
    {
        Pool memory pool = getPoolFinder().getBestWethPool(tokenAddress);
        if (pool.poolAddress == address(0)) revert PoolDoesNotExist();
        isV3Pool    = pool.isV3Pool != 0;
        poolAddress = pool.poolAddress;
    }
}

abstract contract PriceHelper is POW, Storage, PoolFinder {
    error NotBasePool();
    error ZeroLiquidity();

    /// @notice Base-per-1-token price (1e18 scaled) from a Uniswap V2-like pair.
    /// @dev `baseAddress` must be either token0 or token1 in the pair.
    ///      Returns base units per 1 unit of the *other* token in the pool.
    function _getTokenPriceV2(address poolAddress, address baseAddress) public view returns (uint256 price18) {
        IUniswapV2Pair pool = IUniswapV2Pair(poolAddress);
        address t0 = pool.token0();
        address t1 = pool.token1();

        (uint112 r0, uint112 r1, ) = pool.getReserves();

        bool baseIs0 = (t0 == baseAddress);
        bool baseIs1 = (t1 == baseAddress);
        if (!(baseIs0 || baseIs1) || (baseIs0 && baseIs1)) revert NotBasePool();

        if (baseIs0) {
            // token = t1
            uint8 baseDec  = IERC20Metadata(t0).decimals();
            uint8 tokenDec = IERC20Metadata(t1).decimals();
            price18 = _price18(uint256(r0), baseDec, uint256(r1), tokenDec);
        } else {
            // base = t1, token = t0
            uint8 baseDec  = IERC20Metadata(t1).decimals();
            uint8 tokenDec = IERC20Metadata(t0).decimals();
            price18 = _price18(uint256(r1), baseDec, uint256(r0), tokenDec);
        }
    }

    /// @notice Base-per-1-token price (1e18 scaled) from a Uniswap V3 pool via slot0.
    /// @dev `baseAddress` must be one side. Uses sqrtPriceX96 and handles decimals exactly.
    function _getTokenPriceV3(address poolAddress, address baseAddress) public view returns (uint256 price18) {
        IUniswapV3Pool pool = IUniswapV3Pool(poolAddress);
        (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
        if (pool.liquidity() == 0) revert ZeroLiquidity();

        address t0 = pool.token0();
        address t1 = pool.token1();

        bool baseIs0 = (t0 == baseAddress);
        bool baseIs1 = (t1 == baseAddress);
        if (!(baseIs0 || baseIs1) || (baseIs0 && baseIs1)) revert NotBasePool();

        // P_raw in Q96: token1/token0 * 2^96
        uint256 priceQ96 = FullMath.mulDiv(uint256(sqrtPriceX96), uint256(sqrtPriceX96), 1 << 96);

        if (baseIs1) {
            // base = t1, token = t0: want (token1/token0)
            uint8 baseDec  = IERC20Metadata(t1).decimals();
            uint8 tokenDec = IERC20Metadata(t0).decimals();
            unchecked {
                if (tokenDec >= baseDec) {
                    uint256 scale = WAD * _pow10(tokenDec - baseDec);
                    return FullMath.mulDiv(priceQ96, scale, 1 << 96);
                } else {
                    uint256 denom = (1 << 96) * _pow10(baseDec - tokenDec);
                    return FullMath.mulDiv(priceQ96, WAD, denom);
                }
            }
        } else {
            // base = t0, token = t1: want 1 / (token1/token0)
            uint8 baseDec  = IERC20Metadata(t0).decimals();
            uint8 tokenDec = IERC20Metadata(t1).decimals();
            unchecked {
                // **FIX STARTS HERE** (use tokenDec - baseDec, not baseDec - tokenDec)
                if (tokenDec >= baseDec) {
                    // price18 = (1e18 * 10^(tokenDec - baseDec) * 2^96) / priceQ96
                    uint256 numer = WAD * _pow10(tokenDec - baseDec) * (1 << 96);
                    return FullMath.mulDiv(numer, 1, priceQ96);
                } else {
                    // price18 = (1e18 * 2^96) / (priceQ96 * 10^(baseDec - tokenDec))
                    uint256 denom = priceQ96 * _pow10(baseDec - tokenDec);
                    return FullMath.mulDiv(WAD * (1 << 96), 1, denom);
                }
                // **FIX ENDS HERE**
            }
        }
    }



    function _getTokenPrice(address poolAddress, bool isV3Pool, address baseAddress) internal view returns (uint256 price18) {
        if (isV3Pool) {
            price18 = _getTokenPriceV3(poolAddress, baseAddress);
        } else {
            price18 = _getTokenPriceV2(poolAddress, baseAddress);
        }
    }
}

abstract contract WethUsdHelper is PriceHelper {

    function _getWethUsdPrice() internal view returns (uint256 price18) {
        price18 = _getTokenPrice(WETH_USD_PAIR_ADDRESS, IS_WETH_USD_PAIR_ADDRESS_V3, USD_ADDRESS);
    }

    /// @dev Convert a WETH-denominated 1e18 amount to USD 1e18 using the oracle.
    ///      Returns 0 if either the amount or the oracle price is 0.
    function _convertWethToUsd(uint256 wethAmount18) internal view returns (uint256 usdAmount18) {
        if (wethAmount18 == 0) return 0;
        uint256 wethUsdPrice18 = _getWethUsdPrice(); // USD per 1 WETH, 1e18
        if (wethUsdPrice18 == 0) return 0;
        // USD = WETH * (USD/WETH)
        return FullMath.mulDiv(wethAmount18, wethUsdPrice18, WAD);
    }
}

abstract contract LiquidityHelper is WethUsdHelper {
    /// @dev Combines base and token reserves into total WETH value.
    function _calcLiquidityWeth(
        uint256 wethReserve,
        uint8 wethDec,
        uint256 tokenReserve,
        uint8 tokenDec,
        uint256 tokenWethPrice18
    ) internal pure returns (uint256 wethLiquidity18) {
        uint256 wethRes18 = _scaleTo1e18(wethReserve, wethDec);
        uint256 tokenRes18 = _scaleTo1e18(tokenReserve, tokenDec);
        uint256 tokenValueWeth = FullMath.mulDiv(tokenRes18, tokenWethPrice18, WAD);
        wethLiquidity18 = wethRes18 + tokenValueWeth;
    }

    /// @dev Internal helper: Uniswap V2 pool liquidity in WETH terms.
    function _getLiquidityWethV2(address poolAddress) internal view returns (uint256 wethLiquidity18) {
        IUniswapV2Pair pool = IUniswapV2Pair(poolAddress);
        (uint112 r0, uint112 r1, ) = pool.getReserves();
        if (r0 == 0 || r1 == 0) revert EmptyReserves();

        address t0 = pool.token0();
        address t1 = pool.token1();
        bool wethIs0 = (t0 == WETH_ADDRESS);
        bool wethIs1 = (t1 == WETH_ADDRESS);
        if (!(wethIs0 || wethIs1)) revert NotBasePool();

        uint256 tokenWethPrice18 = _getTokenPrice(poolAddress, false, WETH_ADDRESS);

        if (wethIs0) {
            wethLiquidity18 = _calcLiquidityWeth(
                r0,
                IERC20Metadata(t0).decimals(),
                r1,
                IERC20Metadata(t1).decimals(),
                tokenWethPrice18
            );
        } else {
            wethLiquidity18 = _calcLiquidityWeth(
                r1,
                IERC20Metadata(t1).decimals(),
                r0,
                IERC20Metadata(t0).decimals(),
                tokenWethPrice18
            );
        }
    }

    /// @dev Internal helper: Uniswap V3 pool liquidity in WETH terms.
    function _getLiquidityWethV3(address poolAddress) internal view returns (uint256 wethLiquidity18) {
        IUniswapV3Pool pool = IUniswapV3Pool(poolAddress);
        (uint160 sqrtPriceX96, , , , , , ) = pool.slot0();
        uint128 liq = pool.liquidity();
        if (liq == 0) revert ZeroLiquidity();

        address t0 = pool.token0();
        address t1 = pool.token1();
        bool wethIs0 = (t0 == WETH_ADDRESS);
        bool wethIs1 = (t1 == WETH_ADDRESS);
        if (!(wethIs0 || wethIs1)) revert NotBasePool();

        // approximate virtual reserves
        uint256 sqrtP = uint256(sqrtPriceX96);
        uint256 reserve0 = FullMath.mulDiv(uint256(liq) << 96, 1, sqrtP);
        uint256 reserve1 = FullMath.mulDiv(uint256(liq) * sqrtP, 1, 1 << 96);
        uint256 tokenWethPrice18 = _getTokenPrice(poolAddress, true, WETH_ADDRESS);

        if (wethIs0) {
            wethLiquidity18 = _calcLiquidityWeth(
                reserve0,
                IERC20Metadata(t0).decimals(),
                reserve1,
                IERC20Metadata(t1).decimals(),
                tokenWethPrice18
            );
        } else {
            wethLiquidity18 = _calcLiquidityWeth(
                reserve1,
                IERC20Metadata(t1).decimals(),
                reserve0,
                IERC20Metadata(t0).decimals(),
                tokenWethPrice18
            );
        }
    }

    /// @notice Total pool liquidity in WETH for the TOKEN–WETH pair (scaled 1e18).
    /// If TOKEN is WETH, returns WETH totalSupply (scaled to 1e18).
    function _getTokenLiquidityWeth(address tokenAddress) internal view returns (uint256 wethLiquidity18) {
        if (tokenAddress == WETH_ADDRESS) {
            uint256 supply = IERC20(WETH_ADDRESS).totalSupply();
            return _scaleTo1e18(supply, WETH_DECIMALS);
        }

        (bool isV3Pool, address poolAddress) = _findTokenWethPoolAddress(tokenAddress);
        if (isV3Pool) {
            wethLiquidity18 = _getLiquidityWethV3(poolAddress);
        } else {
            wethLiquidity18 = _getLiquidityWethV2(poolAddress);
        }
    }
    
}

contract UniswapOracle is LiquidityHelper {

    constructor(
        address _directory,
        address _wethAddress,
        address _wethUsdPoolAddress,
        bool _isWethUsdPoolV3
    ) ImDirectory(_directory) {
        WETH_ADDRESS = _wethAddress;
        WETH_USD_PAIR_ADDRESS = _wethUsdPoolAddress;
        IS_WETH_USD_PAIR_ADDRESS_V3 = _isWethUsdPoolV3;

        WETH_DECIMALS = IERC20Metadata(_wethAddress).decimals();

        if (_isWethUsdPoolV3) {
            IUniswapV3Pool pool = IUniswapV3Pool(_wethUsdPoolAddress);
            address t0 = pool.token0();
            address t1 = pool.token1();
            USD_ADDRESS = t0 == _wethAddress ? t1 : t0;
        } else {
            IUniswapV2Pair pool = IUniswapV2Pair(_wethUsdPoolAddress);
            address t0 = pool.token0();
            address t1 = pool.token1();
            USD_ADDRESS = t0 == _wethAddress ? t1 : t0;
        }
    }

    // -- Read fns --
    function getWethUsdPrice() public view returns (uint256 price18) {
        return _getWethUsdPrice();
    }

    function getTokenWethPrice(address tokenAddress) public view returns (uint256) {
        if (tokenAddress == WETH_ADDRESS) {
            return WAD;
        }
        (bool isV3Pool, address poolAddress) = _findTokenWethPoolAddress(tokenAddress);

        return _getTokenPrice(poolAddress, isV3Pool, WETH_ADDRESS);
    }

    /// @notice USD price of 1 TOKEN (scaled 1e18).
    function getTokenUsdPrice(address tokenAddress) external view returns (uint256 price18) {
        uint256 tokenWeth = getTokenWethPrice(tokenAddress); // WETH per 1 TOKEN (1e18)
        price18 = _convertWethToUsd(tokenWeth);
    }

    /// @notice Total pool liquidity in WETH for the TOKEN–WETH pair (scaled 1e18).
    /// If TOKEN is WETH, returns WETH totalSupply (scaled to 1e18).
    function getTokenLiquidityWeth(address tokenAddress) public view returns (uint256) {
        return _getTokenLiquidityWeth(tokenAddress);
    }

    /// @notice Total pool liquidity in USD for the TOKEN–WETH pair (scaled 1e18).
    function getTokenLiquidityUsd(address tokenAddress) external view returns (uint256 totalLiquidityUsd18) {
        uint256 totalLiquidityWeth18 = getTokenLiquidityWeth(tokenAddress);
        totalLiquidityUsd18 = _convertWethToUsd(totalLiquidityWeth18);
    }

    /// @notice Market cap in WETH for TOKEN (scaled 1e18): totalSupply * (WETH per 1 TOKEN).
    /// If TOKEN is WETH, this is simply WETH totalSupply (scaled 1e18).
    function getTokenMarketCapWeth(address tokenAddress) public view returns (uint256) {
        uint256 supply  = IERC20(tokenAddress).totalSupply();
        uint8   decT    = IERC20Metadata(tokenAddress).decimals();
        uint256 supply18 = _scaleTo1e18(supply, decT);

        if (tokenAddress == WETH_ADDRESS) {
            return supply18; // price == 1 WETH
        }
        uint256 wethPerToken18 = getTokenWethPrice(tokenAddress);
        return FullMath.mulDiv(supply18, wethPerToken18, WAD);
    }

    /// @notice Market cap in USD for TOKEN (scaled 1e18).
    function getTokenMarketCapUsd(address tokenAddress) external view returns (uint256 marketCapUsd18) {
        uint256 mcWeth18     = getTokenMarketCapWeth(tokenAddress);
        uint256 usdPerWeth18 = getWethUsdPrice();
        marketCapUsd18 = FullMath.mulDiv(mcWeth18, usdPerWeth18, WAD);
    }

    function getTokenWethPoolAddress(address tokenAddress) external view returns (bool isV3Pool, address poolAddress) {
        return _findTokenWethPoolAddress(tokenAddress);
    }

    function emitPoolChangedEventOnWethUsdPool() external {
        getPoolFinder().emitPoolChangedEventOnWethUsdPool(WETH_USD_PAIR_ADDRESS, IS_WETH_USD_PAIR_ADDRESS_V3);
    }
}"
    },
    "hopium/common/lib/full-math.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

library FullMath {
    /// @dev Full-precision multiply-divide: floor(a * b / denominator)
    ///      Reverts if denominator == 0 or the result overflows uint256.
    /// @notice Based on Uniswap v3’s FullMath.mulDiv().
    function mulDiv(
        uint256 a,
        uint256 b,
        uint256 denominator
    ) internal pure returns (uint256 result) {
        unchecked {
            // 512-bit multiply [prod1 prod0] = a * b
            uint256 prod0; // Least-significant 256 bits
            uint256 prod1; // Most-significant 256 bits
            assembly {
                let mm := mulmod(a, b, not(0))
                prod0 := mul(a, b)
                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
            }

            // No overflow: do simple division
            if (prod1 == 0) return prod0 / denominator;

            require(denominator > prod1, "mulDiv overflow");

            ///////////////////////////////////////////////
            //  Make division exact  (subtract remainder)
            ///////////////////////////////////////////////
            uint256 remainder;
            assembly {
                remainder := mulmod(a, b, denominator)
                prod1 := sub(prod1, gt(remainder, prod0))
                prod0 := sub(prod0, remainder)
            }

            ///////////////////////////////////////////////
            //  Factor powers of two out of denominator
            ///////////////////////////////////////////////
            uint256 twos = denominator & (~denominator + 1);
            assembly {
                denominator := div(denominator, twos)
                prod0 := div(prod0, twos)
                twos := add(div(sub(0, twos), twos), 1)
            }

            // Combine high and low products
            prod0 |= prod1 * twos;

            ///////////////////////////////////////////////
            //  Compute modular inverse of denominator mod 2²⁵⁶
            ///////////////////////////////////////////////
            uint256 inv = (3 * denominator) ^ 2;
            inv *= 2 - denominator * inv; // inverse mod 2⁸
            inv *= 2 - denominator * inv; // mod 2¹⁶
            inv *= 2 - denominator * inv; // mod 2³²
            inv *= 2 - denominator * inv; // mod 2⁶⁴
            inv *= 2 - denominator * inv; // mod 2¹²⁸
            inv *= 2 - denominator * inv; // mod 2²⁵⁶

            ///////////////////////////////////////////////
            //  Multiply by modular inverse to finish division
            ///////////////////////////////////////////////
            result = prod0 * inv;
            return result;
        }
    }
}"
    },
    "hopium/uniswap/interface/imPoolFinder.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import "hopium/uniswap/types/pool.sol";
import "hopium/common/interface/imDirectory.sol";

interface IPoolFinder {
    function getBestWethPool(address tokenAddress) external view returns (Pool memory pool);
    function getBestWethPoolAndUpdateIfStale(address tokenAddress) external returns (Pool memory pool);
    function getBestWethPoolAndForceUpdate(address tokenAddress) external returns (Pool memory out);
    function emitPoolChangedEventOnWethUsdPool(address poolAddress, bool isV3Pool) external;
}

abstract contract ImPoolFinder is ImDirectory {

    function getPoolFinder() internal view virtual returns (IPoolFinder) {
        return IPoolFinder(fetchFromDirectory("pool-finder"));
    }
}"
    },
    "hopium/common/interface/imDirectory.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

/// @notice Interface used by the registry to talk to the external directory.
interface IDirectory {
    function owner() external view returns (address);
    function fetchFromDirectory(string memory _key) external view returns (address);
}

abstract contract ImDirectory {
    IDirectory public Directory;

    constructor(address _directory) {
        _setDirectory(_directory); // no modifier here
    }

    function changeDirectoryAddress(address _directory) external onlyOwner {
        _setDirectory(_directory);
    }

    function _setDirectory(address _directory) internal {
        require(_directory != address(0), "Directory cannot be zero address");
        require(_directory.code.length > 0, "Directory must be a contract");

        // Sanity check the interface
        try IDirectory(_directory).owner() returns (address) {
            Directory = IDirectory(_directory);
        } catch {
            revert("Directory address does not implement owner()");
        }
    }

    modifier onlyOwner() {
        require(msg.sender == Directory.owner(), "Caller is not the owner");
        _;
    }

    function owner() public view returns (address) {
        return Directory.owner();
    }

    function fetchFromDirectory(string memory _key) public view returns (address) {
        return Directory.fetchFromDirectory(_key);
    }
}
"
    },
    "hopium/uniswap/types/pool.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

struct Pool {
    address poolAddress; // 20
    uint24  poolFee;     // 3
    uint8   isV3Pool;    // 1 (0/1)
    uint64  lastCached;  // 8
}"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
DeFi, Liquidity, Factory, Oracle|addr:0x90b98e5f6f0786a07894eafdd1cd1729c894047d|verified:true|block:23668163|tx:0x13a6ddddb17999416788e0dca8fc2c51e73b5c5fadba328d78513e6f6b097ed8|first_check:1761567278

Submitted on: 2025-10-27 13:14:39

Comments

Log in to comment.

No comments yet.