CurvePoolRegistry

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",
  "sources": {
    "contracts/swap/CurvePoolRegistry.sol": {
      "content": "// SPDX-License-Identifier: MIT\r
pragma solidity ^0.8.30;\r
\r
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";\r
\r
/**\r
 *⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\r
 *⠀⠀⠀⠀⠈⢻⣿⠛⠻⢷⣄⠀⠀ ⣴⡟⠛⠛⣷⠀ ⠘⣿⡿⠛⠛⢿⡇⠀⠀⠀⠀\r
 *⠀⠀⠀⠀⠀⢸⣿⠀⠀ ⠈⣿⡄⠀⠿⣧⣄⡀ ⠉⠀⠀ ⣿⣧⣀⣀⡀⠀⠀⠀⠀⠀\r
 *⠀⠀⠀⠀⠀⢸⣿⠀⠀ ⢀⣿⠃ ⣀ ⠈⠉⠻⣷⡄⠀ ⣿⡟⠉⠉⠁⠀⠀⠀⠀⠀\r
 *⠀⠀⠀⠀⢠⣼⣿⣤⣴⠿⠋⠀ ⠀⢿⣦⣤⣴⡿⠁ ⢠⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀\r
 *\r
 *      - Defining Successful Future -\r
 *\r
 * @title CurvePoolRegistry\r
 * @author Andrei Averin — CTO dsf.finance\r
 * @notice Centralized, owner-controlled registry for whitelisting safe and verified Curve pools.\r
 * @dev\r
 * This registry is designed to be consumed by aggregator modules (e.g. CurveDexModule).\r
 * It stores:\r
 *  - a per-(tokenA,tokenB) whitelist of pools (strict routing allowlist), and\r
 *  - a minimal call profile for each pool describing how to call its exchange method.\r
 *\r
 * Pair whitelist:\r
 *  - Pools are stored under a canonical key: keccak256(sorted(tokenA, tokenB)).\r
 *  - Use {setVerifiedPools} to atomically set the full allowlisted list for a pair.\r
 *  - Use {clearVerifiedPools} to remove all allowlisted pools for a pair.\r
 *\r
 * Pool profile:\r
 *  - Minimal shape  : {exIndexUint, exHasEthFlag}.\r
 *    exIndexUint    : true  => exchange(uint256,uint256,...) ; false => exchange(int128,int128,...)\r
 *    exHasEthFlag   : true  => non-underlying exchange has a trailing `bool use_eth` flag\r
 *  - Use {setPoolProfile}/{setPoolProfiles} to set/update profiles.\r
 *  - Use {clearPoolProfile} to delete a profile (caller modules may fall back to a modern default).\r
 *\r
 * Access control:\r
 *  - All mutating methods are restricted to the contract owner.\r
 */\r
 \r
/**\r
 * @title IPoolRegistry\r
 * @notice Interface for accessing whitelisted Curve pools.\r
 */\r
interface IPoolRegistry {\r
    struct PoolProfile {\r
        // exchange signatures\r
        bool exIndexUint;           // true: exchange(uint256,uint256,...) ; false: exchange(int128,int128,...)\r
        bool exHasEthFlag;          // true: у non-underlying exchange есть bool use_eth\r
    }\r
\r
    function getVerifiedPools(address tokenA, address tokenB) external view returns (address[] memory);\r
    function getPoolProfile(address pool) external view returns (bool exists, PoolProfile memory profile);\r
}\r
\r
/**\r
 * @title CurvePoolRegistry\r
 * @notice Stores and manages the list of officially verified Curve pools for specific token pairs.\r
 */\r
contract CurvePoolRegistry is Ownable, IPoolRegistry {\r
    /* ───────────────────────────── Events ───────────────────────────── */\r
    \r
    event VerifiedPoolsSet(bytes32 indexed pairKey, address[] pools);\r
    event VerifiedPoolsCleared(bytes32 indexed pairKey);\r
    event PoolAppended(bytes32 indexed pairKey, address pool);\r
    event PoolRemoved(bytes32 indexed pairKey, address pool);\r
\r
    event PoolProfileSet(address indexed pool, bool exIndexUint, bool exHasEthFlag);\r
    event PoolProfileCleared(address indexed pool);\r
\r
    /* ──────────────────────────── Selectors ─────────────────────────── */\r
    \r
    bytes4 private constant COINS_U256      = bytes4(keccak256("coins(uint256)"));\r
    bytes4 private constant COINS_I128      = bytes4(keccak256("coins(int128)"));\r
    bytes4 private constant EX_U256_ETH     = bytes4(keccak256("exchange(uint256,uint256,uint256,uint256,bool)"));\r
    bytes4 private constant EX_U256         = bytes4(keccak256("exchange(uint256,uint256,uint256,uint256)"));\r
    bytes4 private constant EX_I128_ETH     = bytes4(keccak256("exchange(int128,int128,uint256,uint256,bool)"));\r
    bytes4 private constant EX_I128         = bytes4(keccak256("exchange(int128,int128,uint256,uint256)"));\r
\r
    /* ───────────────────────────── Storage ──────────────────────────── */\r
\r
    // key: keccak256(sorted(tokenA, tokenB)) => allowlisted pools\r
    mapping(bytes32 => address[]) public verifiedPools;\r
\r
    // per-pool profile\r
    mapping(address => PoolProfile) private _profiles;\r
    mapping(address => bool)        private _hasProfile;\r
\r
    constructor() Ownable(msg.sender) {}\r
\r
    /**\r
     * @notice Returns the list of verified Curve pools for a token pair.\r
     * @param  tokenA  The first token in the pair.\r
     * @param  tokenB  The second token in the pair.\r
     * @return List of verified pools found.\r
     */\r
    function getVerifiedPools(address tokenA, address tokenB) \r
        external \r
        view \r
        override \r
        returns (address[] memory) \r
    {\r
        return verifiedPools[_pairKey(tokenA, tokenB)];\r
    }\r
\r
    /**\r
     * @notice Returns the call profile for a specific pool, if specified.\r
     * @dev    If the profile is not found, `exists` will be `false` and `profile` will be empty (default).\r
     *         Use `exists` to check for the presence of a custom profile and select the signature\r
     *         `exchange(...)` in the module.\r
     * @param  pool    Curve pool address.\r
     * @return exists  Flag indicating the existence of a saved profile in the registry.\r
     * @return profile Structure { exIndexUint, exHasEthFlag } for selecting the correct signature.\r
     */\r
    function getPoolProfile(address pool)\r
        external\r
        view\r
        override\r
        returns (bool exists, PoolProfile memory profile)\r
    {\r
        if (_hasProfile[pool]) {\r
            return (true, _profiles[pool]);\r
        }\r
        return (false, profile);\r
    }\r
\r
    /**\r
     * @notice Flat version of the profile: separately, without tuple\r
     * @return exists       Is there a saved profile\r
     * @return exIndexUint  true => exchange(uint256,...), false => exchange(int128,...)\r
     * @return exHasEthFlag true => non-underlying exchange has trailing bool use_eth\r
     */\r
    function getPoolProfileFlat(address pool)\r
        external\r
        view\r
        returns (bool exists, bool exIndexUint, bool exHasEthFlag)\r
    {\r
        if (_hasProfile[pool]) {\r
            IPoolRegistry.PoolProfile memory p = _profiles[pool];\r
            return (true, p.exIndexUint, p.exHasEthFlag);\r
        }\r
        return (false, false, false);\r
    }\r
    \r
    /* ──────────────────────── Mutations (pairs) ─────────────────────── */\r
\r
    /**\r
     * @notice Adds or replaces the list of whitelisted Curve pools for a token pair.\r
     * @param  tokenA The first token in the pair.\r
     * @param  tokenB The second token in the pair.\r
     * @param  pools  The list of Curve pools that are allowed to be used for exchanges.\r
     */\r
    function setVerifiedPools(address tokenA, address tokenB, address[] calldata pools)\r
        external\r
        onlyOwner\r
    {\r
        require(pools.length > 0, "Registry: empty list");\r
        bytes32 key = _pairKey(tokenA, tokenB);\r
\r
        for (uint256 i; i < pools.length; ) {\r
            require(pools[i] != address(0), "Registry: zero pool");\r
            unchecked { ++i; }\r
        }\r
        verifiedPools[key] = pools;\r
        emit VerifiedPoolsSet(key, pools);\r
    }\r
    \r
    /**\r
     * @notice Removes all verified pools for a token pair.\r
     */\r
    function clearVerifiedPools(address tokenA, address tokenB)\r
        external\r
        onlyOwner\r
    {\r
        bytes32 key = _pairKey(tokenA, tokenB);\r
        delete verifiedPools[key];\r
        emit VerifiedPoolsCleared(key);\r
    }\r
\r
    /**\r
     * @notice Adds one Curve pool to the allowlist for the pair (tokenA, tokenB).\r
     * @dev\r
     * - The order of addresses in the pair is not important: the canonical key `_pairKey` is used internally.\r
     * - The function is idempotent: if the pool is already in the list, we simply exit without making any changes.\r
     * - Emits the {PoolAppended} event only when actually added.\r
     *\r
     * Details/gas:\r
     * - Duplicate checking is performed by a linear pass through the array (O(n)).\r
     * - The pool address cannot be null.\r
     *\r
     * @param tokenA The first token in the pair.\r
     * @param tokenB The second token in the pair.\r
     * @param pool   The address of the Curve pool to be added to the allowlist.\r
     */\r
    function appendVerifiedPool(address tokenA, address tokenB, address pool)\r
        external\r
        onlyOwner\r
    {\r
        require(pool != address(0), "Registry: zero pool");\r
        bytes32 key = _pairKey(tokenA, tokenB);\r
        address[] storage arr = verifiedPools[key];\r
\r
        for (uint256 i; i < arr.length; ) {\r
            if (arr[i] == pool) return;\r
            unchecked { ++i; }\r
        }\r
        arr.push(pool);\r
        emit PoolAppended(key, pool);\r
    }\r
\r
    /**\r
     * @notice Removes one Curve pool from the allowlist for the pair (tokenA, tokenB).\r
     * @dev\r
     * - The order of addresses in the pair is not important: the canonical key `_pairKey` is used.\r
     * - If the pool is not in the list — no-op (without revert), we simply exit.\r
     * - Removal is performed by swap-remove (moving the last element to the place of the removed one) without preserving the order.\r
     * - Emits the {PoolRemoved} event only upon actual removal.\r
     *\r
     * Details/gas:\r
     * - Element search is linear (O(n)).\r
     * - The order of pools in the array may change after deletion.\r
     *\r
     * @param tokenA The first token of the pair.\r
     * @param tokenB The second token of the pair.\r
     * @param pool   The address of the Curve pool to be removed from the allowlist.\r
     */\r
    function removeVerifiedPool(address tokenA, address tokenB, address pool)\r
        external\r
        onlyOwner\r
    {\r
        bytes32 key = _pairKey(tokenA, tokenB);\r
        address[] storage arr = verifiedPools[key];\r
        uint256 n = arr.length;\r
        for (uint256 i; i < n; ) {\r
            if (arr[i] == pool) {\r
                arr[i] = arr[n - 1];\r
                arr.pop();\r
                emit PoolRemoved(key, pool);\r
                return;\r
            }\r
            unchecked { ++i; }\r
        }\r
    }\r
\r
    /* ────────────────────── Mutations (profiles) ────────────────────── */\r
\r
    /**\r
     * @notice Sets/updates the profile for a single pool.\r
     * @dev    Only available to the owner. Overwrites the existing profile\r
     *         if it has already been set.\r
     * @param  pool Curve pool address (cannot be `address(0)`).\r
     * @param  p    Call profile: \r
     *          - `exIndexUint`   : true => exchange(uint256,uint256,...), false => exchange(int128,int128,...)\r
     *          - `exHasEthFlag`  : true => non-underlying exchange has trailing `bool use_eth`\r
     */\r
    function setPoolProfile(address pool, PoolProfile calldata p) external onlyOwner {\r
        require(pool != address(0), "Registry: zero pool");\r
        _profiles[pool]  = p;\r
        _hasProfile[pool] = true;\r
        emit PoolProfileSet(pool, p.exIndexUint, p.exHasEthFlag);\r
    }\r
\r
    /**\r
     * @notice Batch installation/update of profiles for multiple pools.\r
     * @dev    Only available to the owner. Array lengths must match and be > 0.\r
     *         Each element `pools[i]` receives profile `ps[i]`. Existing profiles\r
     *         are overwritten.\r
     * @param  pools Array of Curve pool addresses.\r
     * @param  ps    Array of profiles for the corresponding pools:\r
     *           - `exIndexUint`  : true => exchange(uint256,uint256,...), false => exchange(int128,int128,...)\r
     *           - `exHasEthFlag` : true => non-underlying exchange has trailing `bool use_eth`\r
     */\r
    function setPoolProfiles(address[] calldata pools, PoolProfile[] calldata ps) external onlyOwner {\r
        uint256 len = pools.length;\r
        require(len == ps.length && len > 0, "Registry: bad arrays");\r
        for (uint256 i; i < len; ) {\r
            address pool = pools[i];\r
            require(pool != address(0), "Registry: zero pool");\r
            _profiles[pool]  = ps[i];\r
            _hasProfile[pool] = true;\r
            emit PoolProfileSet(pool, ps[i].exIndexUint, ps[i].exHasEthFlag);\r
            unchecked { ++i; }\r
        }\r
    }\r
\r
    /**\r
     * @notice Deletes the saved profile for the pool.\r
     * @dev    Only available to the owner. After deletion, modules may\r
     *         switch to the “modern” default or refuse routing —\r
     *         depending on your logic.\r
     * @param  pool Address of the Curve pool for which you want to delete the profile.\r
     */\r
    function clearPoolProfile(address pool) external onlyOwner {\r
        delete _profiles[pool];\r
        delete _hasProfile[pool];\r
        emit PoolProfileCleared(pool);\r
    }\r
\r
    /**\r
     * @notice Detect Curve pool exchange profile (indices type + presence of `use_eth` flag).\r
     * @dev    Heuristic:\r
     *         1) Check `coins(uint256)` / `coins(int128)` as a hint.\r
     *         2) Confirm by probing corresponding `exchange(...)` forms via staticcall.\r
     *         If both hints are ambiguous, both exchange branches are probed.\r
     *         Reverts if no known form is supported.\r
     * @param  pool         Curve pool address to probe (must be nonzero).\r
     * @return exIndexUint  True  => pool uses `exchange(uint256,...)`; False => `exchange(int128,...)`.\r
     * @return exHasEthFlag True  => non-underlying exchange has trailing `bool use_eth` flag.\r
     */\r
    function detectProfile(address pool) external view returns (bool exIndexUint, bool exHasEthFlag) {\r
        require(pool != address(0), "probe: zero pool");\r
\r
        bool coinsU = _supportsCoinsU256(pool);\r
        bool coinsI = _supportsCoinsI128(pool);\r
\r
        // First, we focus on coins(...)\r
        if (coinsU && !coinsI) {\r
            (bool hasEth, bool okAny) = _probeExchangeUint(pool);\r
            if (okAny) return (true, hasEth);\r
            // fallback: suddenly coins were introduced, but the exchange is old — let's check int128\r
            (hasEth, okAny) = _probeExchangeI128(pool);\r
            if (okAny) return (false, hasEth);\r
        } else if (!coinsU && coinsI) {\r
            (bool hasEth, bool okAny) = _probeExchangeI128(pool);\r
            if (okAny) return (false, hasEth);\r
            (hasEth, okAny) = _probeExchangeUint(pool);\r
            if (okAny) return (true, hasEth);\r
        } else {\r
            // Unclear about coins — let's try both exchange branches\r
            (bool hasEthU, bool okU) = _probeExchangeUint(pool);\r
            if (okU) return (true, hasEthU);\r
            (bool hasEthI, bool okI) = _probeExchangeI128(pool);\r
            if (okI) return (false, hasEthI);\r
        }\r
\r
        revert("probe: cannot detect exchange signature");\r
    }\r
\r
    /* ──────────────────────── Internal helpers ──────────────────────── */\r
\r
    /**\r
     * @notice Hashes a pair of tokens into a unique key, independent of the order.\r
     */\r
    function _pairKey(address a, address b) internal pure returns (bytes32) {\r
        return a < b ? keccak256(abi.encodePacked(a, b))\r
                     : keccak256(abi.encodePacked(b, a));\r
    }\r
\r
    /**\r
     * @notice Quick capability check for `coins(uint256)` on a given pool.\r
     * @dev    Uses  low-level staticcall and returns true if the signature exists (does NOT validate return value).\r
     * @param  pool  Curve pool address to probe.\r
     * @return ok    True if `coins(uint256)` staticcall did not revert.\r
     */\r
    function _supportsCoinsU256(address pool) internal view returns (bool ok) {\r
        (ok,) = pool.staticcall(abi.encodeWithSelector(COINS_U256, uint256(0)));\r
    }\r
\r
    /**\r
     * @notice Quick capability check for `coins(int128)` on a given pool.\r
     * @dev    Uses low-level staticcall and returns true if the signature exists (does NOT validate return value).\r
     * @param  pool Curve pool address to probe.\r
     * @return ok   True if `coins(int128)` staticcall did not revert.\r
     */\r
    function _supportsCoinsI128(address pool) internal view returns (bool ok) {\r
        (ok,) = pool.staticcall(abi.encodeWithSelector(COINS_I128, int128(0)));\r
    }\r
\r
    /**\r
     * @notice Probe non-underlying exchange signatures that use uint256 indices.\r
     * @dev    Tries 5-arg form with trailing `bool use_eth` first, then classic 4-arg form.\r
     *         Calls are made with dummy values and checked only for revert/non-revert.\r
     * @param  pool       Curve pool address to probe.\r
     * @return hasEthFlag True if pool accepts 5-arg form with `use_eth`.\r
     * @return okAny      True if at least one uint256-index exchange signature is supported.\r
     */\r
    function _probeExchangeUint(address pool) internal view returns (bool hasEthFlag, bool okAny) {\r
        {\r
            (bool ok,) = pool.staticcall(abi.encodeWithSelector(EX_U256_ETH, uint256(0), uint256(1), uint256(0), uint256(0), false));\r
            if (ok) return (true, true);\r
        }\r
        {\r
            (bool ok,) = pool.staticcall(abi.encodeWithSelector(EX_U256, uint256(0), uint256(1), uint256(0), uint256(0)));\r
            if (ok) return (false, true);\r
        }\r
        return (false, false);\r
    }\r
\r
    /**\r
     * @notice Probe non-underlying exchange signatures that use int128 indices.\r
     * @dev    Tries 5-arg form with trailing `bool use_eth` first, then classic 4-arg form.\r
     *         Calls are made with dummy values and checked only for revert/non-revert.\r
     * @param  pool       Curve pool address to probe.\r
     * @return hasEthFlag True if pool accepts 5-arg form with `use_eth`.\r
     * @return okAny      True if at least one int128-index exchange signature is supported.\r
     */\r
    function _probeExchangeI128(address pool) internal view returns (bool hasEthFlag, bool okAny) {\r
        {\r
            (bool ok,) = pool.staticcall(abi.encodeWithSelector(EX_I128_ETH, int128(0), int128(1), uint256(0), uint256(0), false));\r
            if (ok) return (true, true);\r
        }\r
        {\r
            (bool ok,) = pool.staticcall(abi.encodeWithSelector(EX_I128, int128(0), int128(1), uint256(0), uint256(0)));\r
            if (ok) return (false, true);\r
        }\r
        return (false, false);\r
    }\r
}"
    },
    "@openzeppelin/contracts/access/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol)

pragma solidity ^0.8.20;

import {Context} from "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * The initial owner is set to the address provided by the deployer. This can
 * later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    /**
     * @dev The caller account is not authorized to perform an operation.
     */
    error OwnableUnauthorizedAccount(address account);

    /**
     * @dev The owner is not a valid owner account. (eg. `address(0)`)
     */
    error OwnableInvalidOwner(address owner);

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the address provided by the deployer as the initial owner.
     */
    constructor(address initialOwner) {
        if (initialOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(initialOwner);
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        if (owner() != _msgSender()) {
            revert OwnableUnauthorizedAccount(_msgSender());
        }
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby disabling any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        if (newOwner == address(0)) {
            revert OwnableInvalidOwner(address(0));
        }
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}
"
    },
    "@openzeppelin/contracts/utils/Context.sol": {
      "content": "// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

pragma solidity ^0.8.20;

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}
"
    }
  },
  "settings": {
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "viaIR": true,
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "remappings": []
  }
}}

Tags:
Multisig, Swap, Multi-Signature, Factory|addr:0x421daae227a9bf6b161bb7c7159a0724a6f89585|verified:true|block:23693770|tx:0xa006909dd67d21bbcf9f8cba5122b94a2a243950711b00c4a0ae13b39b036529|first_check:1761905973

Submitted on: 2025-10-31 11:19:33

Comments

Log in to comment.

No comments yet.