PriceOracleReporter

Description:

Proxy contract enabling upgradeable smart contract patterns. Delegates calls to an implementation contract.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/reporter/PriceOracleReporter.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;

import {IReporter} from "./IReporter.sol";
import {Ownable} from "solady/auth/Ownable.sol";

/**
 * @title PriceOracleReporter
 * @notice A reporter contract that allows a trusted party to report the price per share of the strategy
 *         with gradual price transitions to prevent arbitrage opportunities
 */
contract PriceOracleReporter is IReporter, Ownable {
    /*//////////////////////////////////////////////////////////////
                                ERRORS
    //////////////////////////////////////////////////////////////*/

    error InvalidSource();
    error InvalidMaxDeviation();
    error InvalidTimePeriod();

    /*//////////////////////////////////////////////////////////////
                                EVENTS
    //////////////////////////////////////////////////////////////*/

    event ForceCompleteTransition(uint256 roundNumber, uint256 targetPricePerShare);
    event PricePerShareUpdated(
        uint256 roundNumber, uint256 targetPricePerShare, uint256 startPricePerShare, string source
    );
    event SetUpdater(address indexed updater, bool isAuthorized);
    event MaxDeviationUpdated(uint256 oldMaxDeviation, uint256 newMaxDeviation, uint256 timePeriod);

    /*//////////////////////////////////////////////////////////////
                                STATE
    //////////////////////////////////////////////////////////////*/

    /// @notice Current round number
    uint256 public currentRound;

    /// @notice The target price per share that we're transitioning to
    uint256 public targetPricePerShare;

    /// @notice The price per share at the start of the current transition
    uint256 public transitionStartPrice;

    /// @notice The timestamp of the last update
    uint256 public lastUpdateAt;

    /// @notice Maximum percentage price change allowed per time period (in basis points, e.g., 100 = 1%)
    uint256 public maxDeviationPerTimePeriod;

    /// @notice Time period for max deviation (in seconds, e.g., 300 = 5 minutes)
    uint256 public deviationTimePeriod;

    /// @notice Tracks price changes that have been applied in current period (basis points)
    uint256 public appliedChangeInPeriod;

    /// @notice The timestamp when the current tracking period started
    uint256 public periodStartTime;

    /// @notice Mapping of authorized updaters
    mapping(address => bool) public authorizedUpdaters;

    /*//////////////////////////////////////////////////////////////
                               CONSTRUCTOR
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Contract constructor
     * @param initialPricePerShare Initial price per share to report (18 decimals)
     * @param updater Initial authorized updater address
     * @param _maxDeviationPerTimePeriod Maximum percentage change per time period (basis points)
     * @param _deviationTimePeriod Time period in seconds
     */
    constructor(
        uint256 initialPricePerShare,
        address updater,
        uint256 _maxDeviationPerTimePeriod,
        uint256 _deviationTimePeriod
    ) {
        _initializeOwner(msg.sender);
        authorizedUpdaters[updater] = true;

        if (_maxDeviationPerTimePeriod == 0) revert InvalidMaxDeviation();
        if (_deviationTimePeriod == 0) revert InvalidTimePeriod();

        currentRound = 1;
        targetPricePerShare = initialPricePerShare;
        transitionStartPrice = initialPricePerShare;
        lastUpdateAt = block.timestamp;
        periodStartTime = block.timestamp;

        maxDeviationPerTimePeriod = _maxDeviationPerTimePeriod;
        deviationTimePeriod = _deviationTimePeriod;
        appliedChangeInPeriod = 0;
    }

    /*//////////////////////////////////////////////////////////////
                            REPORTING FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Update the reported price per share with gradual transition
     * @dev If a transition is already in progress, it will stop the old transition and start a new one.
     * @param newTargetPricePerShare The new target price per share to transition to (18 decimals)
     * @param source_ The source of the price update
     */
    function update(uint256 newTargetPricePerShare, string calldata source_) external {
        if (!authorizedUpdaters[msg.sender]) revert Unauthorized();
        if (bytes(source_).length == 0) revert InvalidSource();

        // Update the current price based on the ongoing transition
        uint256 currentPrice = getCurrentPrice();

        uint256 newRound = currentRound + 1;
        currentRound = newRound;

        // Reset cumulative tracking if period expired
        if (block.timestamp >= periodStartTime + deviationTimePeriod) {
            periodStartTime = block.timestamp;
            appliedChangeInPeriod = 0;
        }

        // Calculate percentage change from current price
        uint256 changePercent = _calculateChangePercent(currentPrice, newTargetPricePerShare);

        // Check if this change plus previously applied changes would exceed period limit
        uint256 newCumulativeChange = appliedChangeInPeriod + changePercent;
        bool withinDeviation = newCumulativeChange <= maxDeviationPerTimePeriod;

        // Always update target and last update time
        targetPricePerShare = newTargetPricePerShare;
        lastUpdateAt = block.timestamp;

        if (withinDeviation) {
            // Direct update without transition
            transitionStartPrice = newTargetPricePerShare;
            appliedChangeInPeriod = newCumulativeChange;
        } else {
            // Set new target and restart transition from current price
            transitionStartPrice = currentPrice;
        }

        emit PricePerShareUpdated(newRound, newTargetPricePerShare, currentPrice, source_);
    }

    /**
     * @notice Report the current price per share
     * @return The encoded current price per share
     */
    function report() external view override returns (bytes memory) {
        return abi.encode(getCurrentPrice());
    }

    /**
     * @notice Calculate percentage change between two prices
     * @param fromPrice Starting price
     * @param toPrice Target price
     * @return Change percentage in basis points
     */
    function _calculateChangePercent(uint256 fromPrice, uint256 toPrice) private pure returns (uint256) {
        if (toPrice > fromPrice) {
            return ((toPrice - fromPrice) * 10000) / fromPrice;
        } else {
            return ((fromPrice - toPrice) * 10000) / fromPrice;
        }
    }

    /**
     * @notice Get the current price, accounting for gradual transitions
     * @return The current price per share
     */
    function getCurrentPrice() public view returns (uint256) {
        if (transitionStartPrice == targetPricePerShare) {
            return targetPricePerShare;
        }

        // Calculate fractional periods for continuous transitions (basis points precision)
        uint256 timeElapsed = block.timestamp - lastUpdateAt;
        uint256 fractionalPeriods = (timeElapsed * 10000) / deviationTimePeriod;
        uint256 maxAllowedChangePercent = (fractionalPeriods * maxDeviationPerTimePeriod) / 10000;

        // Calculate max allowed change from transition start
        uint256 maxAllowedChange = (transitionStartPrice * maxAllowedChangePercent) / 10000;

        // Apply the change in the correct direction
        if (targetPricePerShare > transitionStartPrice) {
            uint256 maxPrice = transitionStartPrice + maxAllowedChange;
            return maxPrice >= targetPricePerShare ? targetPricePerShare : maxPrice;
        } else {
            // Prevent underflow
            if (maxAllowedChange >= transitionStartPrice) {
                return targetPricePerShare;
            }
            uint256 minPrice = transitionStartPrice - maxAllowedChange;
            return minPrice <= targetPricePerShare ? targetPricePerShare : minPrice;
        }
    }

    /**
     * @notice Get the progress of the current price transition
     * @return percentComplete The completion percentage in basis points (0-10000)
     */
    function getTransitionProgress() external view returns (uint256 percentComplete) {
        if (transitionStartPrice == targetPricePerShare) {
            return 10000; // 100%
        }

        uint256 currentPrice = getCurrentPrice();
        if (currentPrice == targetPricePerShare) {
            return 10000;
        }

        // Calculate total change needed
        uint256 totalChange;
        uint256 progressChange;

        if (targetPricePerShare > transitionStartPrice) {
            totalChange = targetPricePerShare - transitionStartPrice;
            progressChange = currentPrice - transitionStartPrice;
        } else {
            totalChange = transitionStartPrice - targetPricePerShare;
            progressChange = transitionStartPrice - currentPrice;
        }

        return (progressChange * 10000) / totalChange;
    }

    /*//////////////////////////////////////////////////////////////
                            OWNER FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Set whether an address is authorized to update values
     * @param updater Address to modify authorization for
     * @param isAuthorized Whether the address should be authorized
     */
    function setUpdater(address updater, bool isAuthorized) external onlyOwner {
        authorizedUpdaters[updater] = isAuthorized;
        emit SetUpdater(updater, isAuthorized);
    }

    /**
     * @notice Update the maximum deviation parameters
     * @dev On calls to setMaxDeviation, the last deviation time period is considered to
     *      have ended, and a new period starts immediately.
     * @param _maxDeviationPerTimePeriod New maximum percentage change per time period (basis points)
     * @param _deviationTimePeriod New time period in seconds
     */
    function setMaxDeviation(uint256 _maxDeviationPerTimePeriod, uint256 _deviationTimePeriod) external onlyOwner {
        if (_maxDeviationPerTimePeriod == 0) revert InvalidMaxDeviation();
        if (_deviationTimePeriod == 0) revert InvalidTimePeriod();

        // Get current price before changing parameters
        uint256 currentPrice = getCurrentPrice();

        // Update rate limit
        uint256 oldMaxDeviation = maxDeviationPerTimePeriod;
        maxDeviationPerTimePeriod = _maxDeviationPerTimePeriod;
        deviationTimePeriod = _deviationTimePeriod;

        // If in transition, restart from current price to apply new rate
        if (currentPrice != targetPricePerShare) {
            transitionStartPrice = currentPrice;
            lastUpdateAt = block.timestamp;
        }

        // Reset cumulative tracking
        periodStartTime = block.timestamp;
        appliedChangeInPeriod = 0;

        emit MaxDeviationUpdated(oldMaxDeviation, _maxDeviationPerTimePeriod, _deviationTimePeriod);
    }

    /**
     * @notice Force complete the current price transition (emergency function)
     * @dev Only callable by owner
     */
    function forceCompleteTransition() external onlyOwner {
        transitionStartPrice = targetPricePerShare;
        lastUpdateAt = block.timestamp;

        emit ForceCompleteTransition(currentRound, targetPricePerShare);
    }
}
"
    },
    "src/reporter/IReporter.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.25;

/**
 * @title IReporter
 * @notice Interface for reporters that return strategy info
 */
interface IReporter {
    /*//////////////////////////////////////////////////////////////
                            FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /**
     * @notice Report the current value of an asset
     * @return the content of the report
     */
    function report() external view returns (bytes memory);
}
"
    },
    "lib/solady/src/auth/Ownable.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @notice Simple single owner authorization mixin.
/// @author Solady (https://github.com/vectorized/solady/blob/main/src/auth/Ownable.sol)
///
/// @dev Note:
/// This implementation does NOT auto-initialize the owner to `msg.sender`.
/// You MUST call the `_initializeOwner` in the constructor / initializer.
///
/// While the ownable portion follows
/// [EIP-173](https://eips.ethereum.org/EIPS/eip-173) for compatibility,
/// the nomenclature for the 2-step ownership handover may be unique to this codebase.
abstract contract Ownable {
    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                       CUSTOM ERRORS                        */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The caller is not authorized to call the function.
    error Unauthorized();

    /// @dev The `newOwner` cannot be the zero address.
    error NewOwnerIsZeroAddress();

    /// @dev The `pendingOwner` does not have a valid handover request.
    error NoHandoverRequest();

    /// @dev Cannot double-initialize.
    error AlreadyInitialized();

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                           EVENTS                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The ownership is transferred from `oldOwner` to `newOwner`.
    /// This event is intentionally kept the same as OpenZeppelin's Ownable to be
    /// compatible with indexers and [EIP-173](https://eips.ethereum.org/EIPS/eip-173),
    /// despite it not being as lightweight as a single argument event.
    event OwnershipTransferred(address indexed oldOwner, address indexed newOwner);

    /// @dev An ownership handover to `pendingOwner` has been requested.
    event OwnershipHandoverRequested(address indexed pendingOwner);

    /// @dev The ownership handover to `pendingOwner` has been canceled.
    event OwnershipHandoverCanceled(address indexed pendingOwner);

    /// @dev `keccak256(bytes("OwnershipTransferred(address,address)"))`.
    uint256 private constant _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE =
        0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0;

    /// @dev `keccak256(bytes("OwnershipHandoverRequested(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE =
        0xdbf36a107da19e49527a7176a1babf963b4b0ff8cde35ee35d6cd8f1f9ac7e1d;

    /// @dev `keccak256(bytes("OwnershipHandoverCanceled(address)"))`.
    uint256 private constant _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE =
        0xfa7b8eab7da67f412cc9575ed43464468f9bfbae89d1675917346ca6d8fe3c92;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                          STORAGE                           */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev The owner slot is given by:
    /// `bytes32(~uint256(uint32(bytes4(keccak256("_OWNER_SLOT_NOT")))))`.
    /// It is intentionally chosen to be a high value
    /// to avoid collision with lower slots.
    /// The choice of manual storage layout is to enable compatibility
    /// with both regular and upgradeable contracts.
    bytes32 internal constant _OWNER_SLOT =
        0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff74873927;

    /// The ownership handover slot of `newOwner` is given by:
    /// ```
    ///     mstore(0x00, or(shl(96, user), _HANDOVER_SLOT_SEED))
    ///     let handoverSlot := keccak256(0x00, 0x20)
    /// ```
    /// It stores the expiry timestamp of the two-step ownership handover.
    uint256 private constant _HANDOVER_SLOT_SEED = 0x389a75e1;

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                     INTERNAL FUNCTIONS                     */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Override to return true to make `_initializeOwner` prevent double-initialization.
    function _guardInitializeOwner() internal pure virtual returns (bool guard) {}

    /// @dev Initializes the owner directly without authorization guard.
    /// This function must be called upon initialization,
    /// regardless of whether the contract is upgradeable or not.
    /// This is to enable generalization to both regular and upgradeable contracts,
    /// and to save gas in case the initial owner is not the caller.
    /// For performance reasons, this function will not check if there
    /// is an existing owner.
    function _initializeOwner(address newOwner) internal virtual {
        if (_guardInitializeOwner()) {
            /// @solidity memory-safe-assembly
            assembly {
                let ownerSlot := _OWNER_SLOT
                if sload(ownerSlot) {
                    mstore(0x00, 0x0dc149f0) // `AlreadyInitialized()`.
                    revert(0x1c, 0x04)
                }
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Store the new value.
                sstore(ownerSlot, or(newOwner, shl(255, iszero(newOwner))))
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
            }
        } else {
            /// @solidity memory-safe-assembly
            assembly {
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Store the new value.
                sstore(_OWNER_SLOT, newOwner)
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, 0, newOwner)
            }
        }
    }

    /// @dev Sets the owner directly without authorization guard.
    function _setOwner(address newOwner) internal virtual {
        if (_guardInitializeOwner()) {
            /// @solidity memory-safe-assembly
            assembly {
                let ownerSlot := _OWNER_SLOT
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
                // Store the new value.
                sstore(ownerSlot, or(newOwner, shl(255, iszero(newOwner))))
            }
        } else {
            /// @solidity memory-safe-assembly
            assembly {
                let ownerSlot := _OWNER_SLOT
                // Clean the upper 96 bits.
                newOwner := shr(96, shl(96, newOwner))
                // Emit the {OwnershipTransferred} event.
                log3(0, 0, _OWNERSHIP_TRANSFERRED_EVENT_SIGNATURE, sload(ownerSlot), newOwner)
                // Store the new value.
                sstore(ownerSlot, newOwner)
            }
        }
    }

    /// @dev Throws if the sender is not the owner.
    function _checkOwner() internal view virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // If the caller is not the stored owner, revert.
            if iszero(eq(caller(), sload(_OWNER_SLOT))) {
                mstore(0x00, 0x82b42900) // `Unauthorized()`.
                revert(0x1c, 0x04)
            }
        }
    }

    /// @dev Returns how long a two-step ownership handover is valid for in seconds.
    /// Override to return a different value if needed.
    /// Made internal to conserve bytecode. Wrap it in a public function if needed.
    function _ownershipHandoverValidFor() internal view virtual returns (uint64) {
        return 48 * 3600;
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                  PUBLIC UPDATE FUNCTIONS                   */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Allows the owner to transfer the ownership to `newOwner`.
    function transferOwnership(address newOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            if iszero(shl(96, newOwner)) {
                mstore(0x00, 0x7448fbae) // `NewOwnerIsZeroAddress()`.
                revert(0x1c, 0x04)
            }
        }
        _setOwner(newOwner);
    }

    /// @dev Allows the owner to renounce their ownership.
    function renounceOwnership() public payable virtual onlyOwner {
        _setOwner(address(0));
    }

    /// @dev Request a two-step ownership handover to the caller.
    /// The request will automatically expire in 48 hours (172800 seconds) by default.
    function requestOwnershipHandover() public payable virtual {
        unchecked {
            uint256 expires = block.timestamp + _ownershipHandoverValidFor();
            /// @solidity memory-safe-assembly
            assembly {
                // Compute and set the handover slot to `expires`.
                mstore(0x0c, _HANDOVER_SLOT_SEED)
                mstore(0x00, caller())
                sstore(keccak256(0x0c, 0x20), expires)
                // Emit the {OwnershipHandoverRequested} event.
                log2(0, 0, _OWNERSHIP_HANDOVER_REQUESTED_EVENT_SIGNATURE, caller())
            }
        }
    }

    /// @dev Cancels the two-step ownership handover to the caller, if any.
    function cancelOwnershipHandover() public payable virtual {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, caller())
            sstore(keccak256(0x0c, 0x20), 0)
            // Emit the {OwnershipHandoverCanceled} event.
            log2(0, 0, _OWNERSHIP_HANDOVER_CANCELED_EVENT_SIGNATURE, caller())
        }
    }

    /// @dev Allows the owner to complete the two-step ownership handover to `pendingOwner`.
    /// Reverts if there is no existing ownership handover requested by `pendingOwner`.
    function completeOwnershipHandover(address pendingOwner) public payable virtual onlyOwner {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute and set the handover slot to 0.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            let handoverSlot := keccak256(0x0c, 0x20)
            // If the handover does not exist, or has expired.
            if gt(timestamp(), sload(handoverSlot)) {
                mstore(0x00, 0x6f5e8818) // `NoHandoverRequest()`.
                revert(0x1c, 0x04)
            }
            // Set the handover slot to 0.
            sstore(handoverSlot, 0)
        }
        _setOwner(pendingOwner);
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                   PUBLIC READ FUNCTIONS                    */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Returns the owner of the contract.
    function owner() public view virtual returns (address result) {
        /// @solidity memory-safe-assembly
        assembly {
            result := sload(_OWNER_SLOT)
        }
    }

    /// @dev Returns the expiry timestamp for the two-step ownership handover to `pendingOwner`.
    function ownershipHandoverExpiresAt(address pendingOwner)
        public
        view
        virtual
        returns (uint256 result)
    {
        /// @solidity memory-safe-assembly
        assembly {
            // Compute the handover slot.
            mstore(0x0c, _HANDOVER_SLOT_SEED)
            mstore(0x00, pendingOwner)
            // Load the handover slot.
            result := sload(keccak256(0x0c, 0x20))
        }
    }

    /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
    /*                         MODIFIERS                          */
    /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

    /// @dev Marks a function as only callable by the owner.
    modifier onlyOwner() virtual {
        _checkOwner();
        _;
    }
}
"
    }
  },
  "settings": {
    "remappings": [
      "forge-std/=lib/forge-std/src/",
      "solady/=lib/solady/src/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "cancun",
    "viaIR": false
  }
}}

Tags:
Proxy, Upgradeable, Factory|addr:0xf92ddf03d3f5ab598eefd4bb3754a38286b3ec04|verified:true|block:23628611|tx:0x17e4249c30eee8a5562e55294c3d083c1caf13044066fb89a39d2eeecb1468b4|first_check:1761230057

Submitted on: 2025-10-23 16:34:19

Comments

Log in to comment.

No comments yet.