EthWentUpResolver

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/EthWentUpResolver.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/// -----------------------------
/// Chainlink ETH/USD interface
/// -----------------------------
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
        );
}

/// -----------------------------
/// PAMM interface (minimal)
/// -----------------------------
interface IPAMM {
    struct PMTuning {
        uint32 lateRampStart; // seconds before close to start ramp (0 = off)
        uint16 lateRampMaxBps; // +bps at T
        uint16 extremeMaxBps; // +bps at extremes
    }

    function createMarketWithPMTuning(
        string calldata description,
        address resolver,
        uint72 close,
        bool canClose,
        uint256 seedYes,
        uint256 seedNo,
        PMTuning calldata t
    ) external returns (uint256 marketId, uint256 noId);

    function setResolverFeeBps(uint16 bps) external;

    function resolve(uint256 marketId, bool outcome) external;

    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
        );
}

/// --------------------------------------------------------------------------
/// EthWentUpResolver
///  • YES if ETH/USD is strictly higher at the first Chainlink update
///    on/after the market’s scheduled resolve time than it was at market
///    creation time. Ties => NO.
///  • When a market is successfully resolved, pays a fixed tip to the caller
///    and immediately creates the next market (perpetual cadence).
///  • New markets: close = now + 1h, resolve = now + 4h (init default).
///  • Owner can tune time knobs, seeds, PMTuning, per-resolve tip, and PAMM
///    resolver fee (fee paid from PAMM pot, not from this contract).
/// --------------------------------------------------------------------------
contract EthWentUpResolver {
    /* ────────────────────────────── constants ───────────────────────────── */

    IPAMM public constant PAMM = IPAMM(0x000000000071176401AdA1f2CD7748e28E173FCa);

    address constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0;
    address constant ZSTETH = 0x000000000077B216105413Dc45Dc6F6256577c7B;

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

    // For descriptions
    string public constant FEED_ENS = "eth-usd.data.eth";

    /* ───────────────────────────── owner/config ─────────────────────────── */

    event OwnershipTransferred(address indexed from, address indexed to);

    address public owner;

    error Unauthorized();

    modifier onlyOwner() {
        if (msg.sender != owner) revert Unauthorized();
        _;
    }

    function transferOwnership(address _owner) public payable onlyOwner {
        emit OwnershipTransferred(msg.sender, owner = _owner);
    }

    /// @dev Staking function into Lido wstETH for ETH inside this contract.
    function stakeETH(uint256 amount) public payable onlyOwner {
        assembly ("memory-safe") {
            pop(call(gas(), WSTETH, amount, codesize(), 0x00, codesize(), 0x00))
        }
    }

    /// @dev Governed exact-in swap to redeem yield from Lido staking.
    function swapExactWSTETHtoETH(address to, uint256 amount, uint256 minOut)
        public
        payable
        onlyOwner
    {
        EthWentUpResolver(payable(ZSTETH)).swapExactWSTETHtoETH(to, amount, minOut);
    }

    /// @dev Governed exact-out swap to redeem yield from Lido staking.
    function swapWSTETHtoExactETH(address to, uint256 exactOut, uint256 maxIn)
        public
        payable
        onlyOwner
    {
        EthWentUpResolver(payable(ZSTETH)).swapWSTETHtoExactETH(to, exactOut, maxIn);
    }

    // Soledge reentrancy guard:
    // (https://github.com/Vectorized/soledge/blob/main/src/utils/ReentrancyGuard.sol)

    error Reentrancy();

    uint256 constant REENTRANCY_GUARD_SLOT = 0x929eee149b4bd21268;

    modifier nonReentrant() {
        assembly ("memory-safe") {
            if tload(REENTRANCY_GUARD_SLOT) {
                mstore(0x00, 0xab143c06) // `Reentrancy()`
                revert(0x1c, 0x04)
            }
            tstore(REENTRANCY_GUARD_SLOT, address())
        }
        _;
        assembly ("memory-safe") {
            tstore(REENTRANCY_GUARD_SLOT, 0)
        }
    }

    // PoC defaults chosen to fit a 4h market with a 1h trading window
    uint256 public closeDelay = 1 hours; // trading open window
    uint256 public resolveDelay = 4 hours; // scheduled market resolution
    uint256 public maxResolveDelay = 6 hours; // cap to limit cherry-picking
    uint256 public maxStale = 25 hours; // Chainlink liveness guard

    // Keeper incentive (fixed per successful resolve)
    uint256 public tipPerResolve = 0.001 ether;

    // Initial symmetric seeds for YES/NO
    uint256 public seedYes = 1e18;
    uint256 public seedNo = 1e18;

    // PMTuning defaults (safe, modest)
    IPAMM.PMTuning public pmTuningDefaults = IPAMM.PMTuning({
        lateRampStart: 15 minutes, // start ramp 15m before close
        lateRampMaxBps: 100, // +1.00% EV charge at/after close
        extremeMaxBps: 50 // up to +0.50% near p≈0 or 1
    });

    /* ────────────────────────────── market state ────────────────────────── */

    struct Epoch {
        // schedule
        uint72 closeAt; // market trading close (creation + closeDelay)
        uint72 resolveAt; // scheduled oracle resolve time (creation + resolveDelay)
        // start observation (captured at market creation)
        uint8 startDecimals;
        uint80 startRoundId;
        uint256 startPrice; // scaled with startDecimals
        uint256 startUpdatedAt;
        bool exists;
        bool resolved;
    }

    // Active market we expect to resolve next
    uint256 public currentMarketId;

    // Per-market state
    mapping(uint256 => Epoch) public epochs;

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

    event MarketStarted(
        uint256 indexed marketId,
        uint72 closeAt,
        uint72 resolveAt,
        uint256 startPrice,
        uint8 startDecimals,
        uint256 startUpdatedAt,
        string description
    );

    event MarketResolved(
        uint256 indexed marketId,
        bool outcome,
        uint256 endPrice,
        uint8 endDecimals,
        uint256 endUpdatedAt,
        uint256 tipPaid
    );

    event Rolled(uint256 indexed fromMarket, uint256 indexed toMarket);

    event Funded(address indexed from, uint256 amount);
    event TipPerResolveSet(uint256 newTip);
    event TimeParamsSet(
        uint256 closeDelay, uint256 resolveDelay, uint256 maxResolveDelay, uint256 maxStale
    );
    event SeedsSet(uint256 seedYes, uint256 seedNo);
    event PMTuningSet(uint32 lateRampStart, uint16 lateRampMaxBps, uint16 extremeMaxBps);
    event PammResolverFeeSet(uint16 bps);

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

    error ActiveMarketLive();
    error NoActiveMarket();
    error AlreadyResolved();
    error ChainlinkAnswerZero();
    error ChainlinkStale();
    error ChainlinkTooSoon(); // latest.updatedAt < resolveAt
    error ResolveWindowExceeded(); // latest.updatedAt > resolveAt + maxResolveDelay
    error ChainlinkInvariant(); // answeredInRound < roundId
    error BadParams();
    error TipPaymentFailed();

    /* ─────────────────────────── constructor ────────────────────────────── */

    constructor() payable {
        IERC20(WSTETH).approve(ZSTETH, type(uint256).max);
        emit OwnershipTransferred(address(0), owner = msg.sender);
    }

    /* ─────────────────────── public funding (tips) ──────────────────────── */

    /// @notice Fund the tip pool used to pay `tipPerResolve` to the first
    ///         successful resolver caller for each market.
    function fundTips() public payable {
        emit Funded(msg.sender, msg.value);
    }

    /// Back-compat alias (same behavior as fundTips)
    function tipResolve() public payable {
        emit Funded(msg.sender, msg.value);
    }

    receive() external payable {
        emit Funded(msg.sender, msg.value);
    }

    /* ──────────────────── owner configuration knobs ─────────────────────── */

    function setTipPerResolve(uint256 newTip) public payable onlyOwner {
        tipPerResolve = newTip;
        emit TipPerResolveSet(newTip);
    }

    /// @param _closeDelay  seconds from market creation to trading close (≥ 5m)
    /// @param _resolveDelay seconds from market creation to scheduled resolution (must exceed closeDelay)
    /// @param _maxResolveDelay max seconds after resolveAt we still accept the first post-resolve update (0 = disable cap)
    /// @param _maxStale  Chainlink liveness guard
    function setTimeParams(
        uint256 _closeDelay,
        uint256 _resolveDelay,
        uint256 _maxResolveDelay,
        uint256 _maxStale
    ) public payable onlyOwner {
        if (_closeDelay < 5 minutes || _resolveDelay <= _closeDelay) revert BadParams();
        // pmTuning.lateRampStart (seconds before close) must be <= closeDelay; we enforce when setting PMTuning too
        closeDelay = _closeDelay;
        resolveDelay = _resolveDelay;
        maxResolveDelay = _maxResolveDelay;
        maxStale = _maxStale;
        emit TimeParamsSet(_closeDelay, _resolveDelay, _maxResolveDelay, _maxStale);
    }

    function setSeeds(uint256 _seedYes, uint256 _seedNo) public payable onlyOwner {
        if ((_seedYes == 0) != (_seedNo == 0)) revert BadParams(); // both zero or both non-zero
        seedYes = _seedYes;
        seedNo = _seedNo;
        emit SeedsSet(_seedYes, _seedNo);
    }

    function setPMTuning(IPAMM.PMTuning calldata t) public payable onlyOwner {
        // Respect PAMM caps/assumptions
        if (t.lateRampMaxBps > 2_000 || t.extremeMaxBps > 2_000) revert BadParams();
        if (t.lateRampStart != 0 && t.lateRampStart > closeDelay) revert BadParams();
        pmTuningDefaults = t;
        emit PMTuningSet(t.lateRampStart, t.lateRampMaxBps, t.extremeMaxBps);
    }

    /// @notice Sets PAMM resolver fee (paid from market pot when PAMM resolves).
    ///         This sets the fee for THIS resolver address in PAMM.
    function setPammResolverFeeBps(uint16 bps) public payable onlyOwner {
        // PAMM enforces bps <= 1000 (10%) internally; we can be lenient here.
        PAMM.setResolverFeeBps(bps);
        emit PammResolverFeeSet(bps);
    }

    /* ───────────────────── lifecycle: start / resolve / roll ───────────── */

    /// @notice Bootstrap the first market (only once, or after a gap).
    function startNewMarket() public payable onlyOwner {
        // Prevent accidental override if the active market hasn’t resolved yet.
        if (currentMarketId != 0 && !epochs[currentMarketId].resolved) revert ActiveMarketLive();
        _startNextMarket();
    }

    /// @notice Resolves the current market if ready, pays the keeper tip,
    ///         and immediately starts the next market.
    function resolve() public payable nonReentrant {
        uint256 mId = currentMarketId;
        if (mId == 0) revert NoActiveMarket();

        Epoch storage e = epochs[mId];
        if (e.resolved) revert AlreadyResolved();

        // Pull latest Chainlink round
        (uint80 rid, int256 ans,, uint256 upd, uint80 air) = FEED.latestRoundData();
        if (ans <= 0) revert ChainlinkAnswerZero();
        if (upd == 0 || upd + maxStale < block.timestamp) revert ChainlinkStale();
        if (air < rid) revert ChainlinkInvariant();

        // Enforce we use the first observation on/after the scheduled resolve time
        if (upd < e.resolveAt) revert ChainlinkTooSoon();
        if (maxResolveDelay != 0 && upd > uint256(e.resolveAt) + maxResolveDelay) {
            revert ResolveWindowExceeded();
        }

        // Normalize decimals across potential aggregator upgrades
        uint8 endDec = FEED.decimals();
        (uint256 sp, uint256 ep) =
            _normalizedPair(e.startPrice, e.startDecimals, uint256(ans), endDec);

        bool outcome = ep > sp; // ties => NO

        // Call into PAMM (requires market close has passed; PAMM enforces)
        PAMM.resolve(mId, outcome);

        // mark, pay tip, emit
        e.resolved = true;

        uint256 tip = tipPerResolve;
        if (tip != 0) {
            uint256 pay = address(this).balance < tip ? address(this).balance : tip;
            if (pay != 0) {
                (bool ok,) = msg.sender.call{value: pay}("");
                if (!ok) revert TipPaymentFailed();
            }
            emit MarketResolved(mId, outcome, uint256(ans), endDec, upd, pay);
        } else {
            emit MarketResolved(mId, outcome, uint256(ans), endDec, upd, 0);
        }

        // Immediately roll to the next market
        uint256 nextId = _startNextMarket();
        emit Rolled(mId, nextId);
    }

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

    function currentTimes()
        public
        view
        returns (uint256 marketId, uint72 closeAt, uint72 resolveAt, bool isResolved)
    {
        marketId = currentMarketId;
        if (marketId != 0) {
            Epoch storage e = epochs[marketId];
            closeAt = e.closeAt;
            resolveAt = e.resolveAt;
            isResolved = e.resolved;
        }
    }

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

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

    function _startNextMarket() internal returns (uint256 newMarketId) {
        // 1) Sample Chainlink for the "start" observation of the new epoch
        (uint80 rid, int256 ans,, uint256 upd, uint80 air) = FEED.latestRoundData();
        if (ans <= 0) revert ChainlinkAnswerZero();
        if (upd == 0 || upd + maxStale < block.timestamp) revert ChainlinkStale();
        if (air < rid) revert ChainlinkInvariant();

        uint8 sDec = FEED.decimals();

        // 2) Schedule
        uint72 closeAt = uint72(block.timestamp + closeDelay);
        uint72 resolveAt = uint72(block.timestamp + resolveDelay);

        // 3) Programmatic description (include resolveAt for unique marketId)
        string memory desc = string(
            abi.encodePacked(
                "ETH price went up vs USD? Resolved by Chainlink (",
                FEED_ENS,
                ") at unix=",
                _u2s(resolveAt),
                " | YES if price > creation snapshot."
            )
        );

        // 4) Create market on PAMM with defaults
        (newMarketId, /*noId*/ ) = PAMM.createMarketWithPMTuning(
            desc,
            address(this),
            closeAt,
            /*canClose=*/
            false,
            seedYes,
            seedNo,
            pmTuningDefaults
        );

        // 5) Persist epoch data
        Epoch storage e = epochs[newMarketId];
        e.closeAt = closeAt;
        e.resolveAt = resolveAt;
        e.startDecimals = sDec;
        e.startRoundId = rid;
        e.startPrice = uint256(ans);
        e.startUpdatedAt = upd;
        e.exists = true;
        e.resolved = false;

        // 6) If no active market, set it
        if (currentMarketId == 0 || epochs[currentMarketId].resolved) {
            currentMarketId = newMarketId;
        } else {
            // If one is active but unresolved, we are creating a parallel market (unusual).
            // We still advance currentMarketId if the previous is resolved already.
            currentMarketId = newMarketId;
        }

        emit MarketStarted(newMarketId, closeAt, resolveAt, uint256(ans), sDec, upd, desc);
    }

    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);
        }
    }

    function _u2s(uint256 x) internal pure returns (string memory s) {
        if (x == 0) return "0";
        uint256 j = x;
        uint256 len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory b = new bytes(len);
        uint256 k = len;
        while (x != 0) {
            k--;
            b[k] = bytes1(uint8(48 + x % 10));
            x /= 10;
        }
        s = string(b);
    }
}

/// @dev Minimal ERC20 token interface.
interface IERC20 {
    function approve(address to, uint256 amount) external returns (bool);
}
"
    }
  },
  "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:
Proxy, Swap, Staking, Yield, Upgradeable, Factory, Oracle|addr:0x40cc6f9ca737a0aa746b645cfc92a67942162cc3|verified:true|block:23554410|tx:0x39d6ef117b1ec3a4754731b1be43da1ba1fb057f9a667513ed46eceec2c602a5|first_check:1760270250

Submitted on: 2025-10-12 13:57:33

Comments

Log in to comment.

No comments yet.