EthWentUpResolver

Description:

Smart contract deployed on Ethereum with Factory, Oracle features.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/EthWentUpResolver.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// @notice Minimal interface for Chainlink AggregatorV3
interface AggregatorV3Interface {
    function decimals() external view returns (uint8);
    function description() external view returns (string memory);
    function latestRoundData()
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );
}

/// @notice Minimal surface of the PAMM we call
interface IPAMM {
    function getMarket(uint256 marketId)
        external
        view
        returns (
            uint256 yesSupply,
            uint256 noSupply,
            address resolver,
            bool resolved,
            bool outcome,
            uint256 pot,
            uint256 payoutPerShare,
            string memory desc,
            uint72 closeTs,
            bool canClose,
            uint256 rYes,
            uint256 rNo,
            uint256 pYes_num,
            uint256 pYes_den
        );

    function resolve(uint256 marketId, bool outcome) external;
}

/// @title EthWentUpResolver
/// @notice YES if ETH/USD is strictly higher at the first Chainlink update on/after
///         (deploy + 30 minutes) than it was at deployment. Ties => NO.
///         Includes a simple tip pot paid to the first successful resolver caller.
contract EthWentUpResolver {
    /* ───────── constants (PoC defaults) ───────── */

    // PAMM singleton
    IPAMM public constant PAMM = IPAMM(0x000000000071176401AdA1f2CD7748e28E173FCa);

    // Chainlink ETH/USD proxy (Ethereum mainnet)
    AggregatorV3Interface public constant FEED =
        AggregatorV3Interface(0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419);

    // Liveness / timing knobs
    uint256 public constant MAX_STALE = 25 hours; // tolerate daily heartbeats
    uint256 public constant MAX_RESOLVE_DELAY = 6 hours; // limit cherry-pick window

    /* ───────── immutable runtime values ───────── */

    address public immutable OWNER;
    uint256 public immutable DEPLOYED_AT;
    uint256 public immutable RESOLVE_AT; // DEPLOYED_AT + 30 minutes

    /* ───────── start observation ───────── */

    uint8 public immutable startDecimals;
    uint80 public startRoundId;
    uint256 public startPrice; // raw feed answer (scale = startDecimals)
    uint256 public startUpdatedAt; // unix seconds

    /* ───────── end observation (filled at resolve) ───────── */

    uint8 public endDecimals;
    uint80 public endRoundId;
    uint256 public endPrice; // raw feed answer (scale = endDecimals)
    uint256 public endUpdatedAt;

    /* ───────── PAMM wiring ───────── */

    uint256 public marketId; // 0 until linked
    bool public resolved;

    /* ───────── events ───────── */

    event Linked(uint256 indexed marketId);
    event Resolved(
        uint256 indexed marketId,
        bool outcome,
        uint256 startPrice,
        uint8 startDecimals,
        uint256 startUpdatedAt,
        uint256 endPrice,
        uint8 endDecimals,
        uint256 endUpdatedAt,
        uint256 tipPaid
    );
    event Tipped(address indexed from, uint256 amount);

    /* ───────── errors ───────── */

    error OnlyOwner();
    error AlreadyLinked();
    error NotLinked();
    error TooEarly();
    error AlreadyResolved();
    error FeedAnswerZero();
    error FeedStale();
    error FeedTooSoon(); // no post-RESOLVE_AT update yet
    error ResolveWindowExceeded(); // end update too far after RESOLVE_AT
    error StaleInvariant(); // answeredInRound < roundId
    error InvalidResolverInMarket();
    error NoTip();
    error TipPaymentFailed();

    /* ───────── constructor (no args) ───────── */

    constructor() payable {
        OWNER = msg.sender;
        DEPLOYED_AT = block.timestamp;
        RESOLVE_AT = DEPLOYED_AT + 30 minutes;

        startDecimals = FEED.decimals();

        (uint80 _rid, int256 _ans,, uint256 _upd, uint80 _air) = FEED.latestRoundData();

        if (_ans <= 0) revert FeedAnswerZero();
        if (_upd == 0 || _upd + MAX_STALE < block.timestamp) revert FeedStale();
        if (_air < _rid) revert StaleInvariant();

        startRoundId = _rid;
        startPrice = uint256(_ans);
        startUpdatedAt = _upd;
    }

    /* ───────── wiring ───────── */

    /// @notice One-time link to a PAMM market that already sets this contract as resolver.
    function link(uint256 _marketId) public payable {
        if (msg.sender != OWNER) revert OnlyOwner();
        if (marketId != 0) revert AlreadyLinked();

        (,, address res,,,,,,,,,,,) = PAMM.getMarket(_marketId);
        if (res != address(this)) revert InvalidResolverInMarket();

        marketId = _marketId;
        emit Linked(_marketId);
    }

    /* ───────── incentives ───────── */

    /// @notice Add ETH tips to pay the first successful resolve() caller.
    function tipResolve() public payable {
        if (msg.value == 0) revert NoTip();
        emit Tipped(msg.sender, msg.value);
    }

    /* ───────── resolution ───────── */

    /// @notice Anyone can call once:
    ///         - block.timestamp >= RESOLVE_AT,
    ///         - the feed has posted an update at/after RESOLVE_AT,
    ///         - and within MAX_RESOLVE_DELAY (limits cherry-picking).
    ///         On success, pays all accumulated tips to the caller.
    function resolve() public payable {
        if (marketId == 0) revert NotLinked();
        if (resolved) revert AlreadyResolved();
        if (block.timestamp < RESOLVE_AT) revert TooEarly();

        (uint80 _rid, int256 _ans,, uint256 _upd, uint80 _air) = FEED.latestRoundData();

        if (_ans <= 0) revert FeedAnswerZero();
        if (_upd == 0 || _upd + MAX_STALE < block.timestamp) revert FeedStale();
        if (_air < _rid) revert StaleInvariant();

        // Require the first post-target observation (prevents pre-RESOLVE_AT comparisons)
        if (_upd < RESOLVE_AT) revert FeedTooSoon();

        // Optional upper bound to reduce "waiting for a nicer candle"
        if (_upd > RESOLVE_AT + MAX_RESOLVE_DELAY) revert ResolveWindowExceeded();

        endDecimals = FEED.decimals();
        endRoundId = _rid;
        endPrice = uint256(_ans);
        endUpdatedAt = _upd;

        // Compare with normalization to handle any future decimals change
        (uint256 sp, uint256 ep) = _normalizedPair(startPrice, startDecimals, endPrice, endDecimals);
        bool outcome = ep > sp; // ties → NO

        // PAMM enforces close-time; if called too early wrt market close, this reverts.
        PAMM.resolve(marketId, outcome);

        resolved = true;

        // Pay tips to the first successful resolver
        uint256 reward = address(this).balance;
        if (reward != 0) {
            (bool ok,) = msg.sender.call{value: reward}("");
            if (!ok) revert TipPaymentFailed();
        }

        emit Resolved(
            marketId,
            outcome,
            startPrice,
            startDecimals,
            startUpdatedAt,
            endPrice,
            endDecimals,
            endUpdatedAt,
            reward
        );
    }

    /* ───────── views ───────── */

    function secondsUntilResolve() public view returns (uint256) {
        return block.timestamp >= RESOLVE_AT ? 0 : (RESOLVE_AT - block.timestamp);
    }

    function feedDescription() public view returns (string memory) {
        return FEED.description();
    }

    /* ───────── internals ───────── */

    function _normalizedPair(uint256 aVal, uint8 aDec, uint256 bVal, uint8 bDec)
        internal
        pure
        returns (uint256 a, uint256 b)
    {
        if (aDec == bDec) return (aVal, bVal);
        if (aDec < bDec) {
            uint256 factor = 10 ** (bDec - aDec);
            return (aVal * factor, bVal);
        } else {
            uint256 factor = 10 ** (aDec - bDec);
            return (aVal, bVal * factor);
        }
    }

    // Accept stray ETH (e.g., self-destruct sends).
    receive() external payable {}
}
"
    }
  },
  "settings": {
    "remappings": [
      "@solady/=lib/solady/",
      "@soledge/=lib/soledge/",
      "@forge/=lib/forge-std/src/",
      "forge-std/=lib/forge-std/src/",
      "solady/=lib/solady/src/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 9999999
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "prague",
    "viaIR": true
  }
}}

Tags:
Factory, Oracle|addr:0xc8e8b5ea222d54d259990b8713bea5ceb0e0122d|verified:true|block:23553604|tx:0xd0cef0c200fbc5ca4682b1c4d24a5ce90b3d14987dea28dac7bb22d7f10cbe0e|first_check:1760266567

Submitted on: 2025-10-12 12:56:10

Comments

Log in to comment.

No comments yet.