PunkLoopEngine

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

/**
 * PunkLoopEngine v2.0 (Mainnet-Final)
 * -----------------------------------
 * Key mechanics:
 * - Presale with soft/hard caps, per-address min/max, 1.69% dev cut (credited via Forwarder)
 * - Direct mint at claim: 60% of cap pro-rata to contributors, 1.69% to dev, remainder to engine
 * - Epoch loop: oracle-guided CryptoPunks buy → commit-reveal RNG → winner choice
 * - Winner choice: 20% of floor in ETH (with 1.69% dev cut) OR 12-month lock
 * - If relisted/sold: 42.069% of proceeds dripped via V4 WETH→PLOOP→burn
 * - Pool eligibility accrual with per-epoch cap (2000 addrs)
 * - Public bounties with owner-tunable amounts and per-epoch cap, reset on each new epoch
 * - Safety: settled-gate before next epoch; “underfunded” guard before buys; nonReentrant on confirms
 * - Hygiene: V4 approve/revoke; min drip; swaps pause; SafeTransferLib for ETH sends
 */

import "./interfaces/ICryptoPunks.sol";
import "./interfaces/IStrategyIndex.sol";
import "./interfaces/IWETH9.sol";
import "./interfaces/IUniswapV4Router.sol";
import "./interfaces/IUniswapV4PoolLite.sol";

import "./ListingOracle.sol";
import "./PloopToken.sol";
import "./V4Forwarder.sol";
import "./libraries/SafeTransferLib.sol";

contract PunkLoopEngine {
    using SafeTransferLib for address;

    // ===== Constants / Params =====
    uint256 public constant CAP_TOKENS = 1_000_000_000 ether;

    // Presale caps: soft 99.42069, hard 420.69
    uint256 public constant PRESALE_SOFT = 99_42069 ether / 1e5;
    uint256 public constant PRESALE_HARD = 420_69 ether / 1e2;

    // Per-address range: 0.0069 - 1.42069
    uint256 public constant PRESALE_MIN = 69 ether / 1e4;
    uint256 public constant PRESALE_MAX = 142069 ether / 1e5;

    uint256 public constant COOLDOWN   = 2 days;
    uint256 public constant PRESALE_DUR= 10 days;

    uint16  public constant DEV_BIPS = 169;            // 1.69%
    uint16  public constant TAX_BIPS = 1000;           // 10% (V4 pool tax, enforced in Forwarder)
    uint16  public constant ETH_CHOICE_BIPS = 2000;    // 20%
    uint32  public constant DEFAULT_BURN_DURATION = 2 hours;   // 2h drip
    uint16  public constant SELL_BURN_BIPS = 42069;    // 42.069%
    uint16  public constant PRESALE_SLICE_BIPS = 3100; // 31% presale slice (epoch >=2)

    // Public bounty defaults (owner-tunable)
    uint256 public constant MAX_BOUNTY_PER_TX_WEI = 0.25 ether;
    uint32  public constant MAX_RESOLVE_STEPS = 1024; // DoS guard for resolveWinnerProgress
    uint32  public constant DEFAULT_MAX_POOL_ADDRS = 2000; // per-epoch cap

    // ===== Admin / wiring =====
    address public owner;
    bool    public enginePaused;

    ICryptoPunks    public PUNKS;
    IStrategyIndex  public strategy;
    ListingOracle   public oracle;
    PloopToken      public token;
    V4Forwarder     public forwarder;
    address         public dev;
    address         public treasury;

    // V4 swap config (set after Epoch 1, then locked)
    address public v4Router;
    address public v4Pool;
    address public WETH;
    bool    public v4SwapLocked;
    bool    public swapsPaused;
    uint256 public minDripWei;

    // ===== Presale =====
    uint40  public psStart;
    uint40  public psEnd;
    uint40  public psFinalized;
    bool    public psSucceeded;
    uint256 public psTotal;
    mapping(address => uint256) public psContrib;
    mapping(address => bool)    public psClaimed;
    bool    public tokensMinted;

    // ===== Epochs =====
    uint256 public epochId;

    struct EpochState {
        uint40  purchasedTs;
        uint40  drawTargetBlock;
        bytes32 saltCommit;
        bytes32 entropy;
        bytes32 poolSnap;
        address winner;
        uint40  winnerDeclaredTs;
        uint40  lastRelistUpdateTs;
        uint256 minBuyWei;
        uint256 punkId;
        uint256 relistFloorWei;
        bool    relisted;
        bool    settled;
    }
    mapping(uint256 => EpochState) public epoch;

    // ===== Eligibility / weights =====
    mapping(address => uint256) public presaleWeight;
    address[] public presaleAddrs;

    mapping(address => bool)    public disqGlobal;

    mapping(uint256 => address[]) public poolAddrs;
    mapping(uint256 => mapping(address => bool)) public seenPool;
    mapping(uint256 => mapping(address => bool)) public disqEpoch;
    bytes32 public cumPoolHash;

    // ===== Lock ownership =====
    struct Lock { uint256 punkId; uint40 unlockTs; }
    mapping(address => Lock) public locked;

    // ===== Burn stream =====
    struct Burn {
        uint256 amount;
        uint40  start;
        uint40  duration;
        uint256 remaining;
    }
    Burn public burn;
    uint40 public burnDuration = DEFAULT_BURN_DURATION;

    // ===== Pool limits =====
    uint32 public maxPoolAddrsPerEpoch = DEFAULT_MAX_POOL_ADDRS;

    // ===== Bounties =====
    // Tunable bounty amounts
    uint256 public bountyRevealWei  = 0.002 ether;
    uint256 public bountyResolveWei = 0.002 ether;
    uint256 public bountyConfirmWei = 0.002 ether;

    // Per-epoch bounty accounting and cap
    mapping(uint256 => uint256) public epochBountySpent;
    uint256 public epochBountyLimit = 0.2 ether;

    // ===== Simple reentrancy guard (for confirm) =====
    bool private _enteredConfirm;
    modifier nonReentrantConfirm() {
        require(!_enteredConfirm, "REENTRANT");
        _enteredConfirm = true;
        _;
        _enteredConfirm = false;
    }

    // ===== Events =====
    event OwnerSet(address indexed owner);
    event EnginePaused(bool paused);
    event WiringSet(
        address punks,
        address strategy,
        address oracle,
        address token,
        address forwarder,
        address dev,
        address treasury
    );

    event SwapConfigSet(address router, address pool, address weth, bool locked);
    event SwapsPaused(bool paused);
    event MinDripWeiSet(uint256 weiAmount);
    event BurnDurationSet(uint40 secondsDuration);
    event MaxPoolAddrsPerEpochSet(uint32 value);

    event PresaleStarted(uint256 start, uint256 end);
    event PresaleContributed(address indexed user, uint256 amount);
    event PresaleFinalized(bool success, uint256 total);
    event PresaleClaimed(address indexed user, uint256 tokenAmount, uint256 weightAdded);
    event Refunded(address indexed user, uint256 amount);

    event EpochPurchased(uint256 indexed epochId, uint256 punkId, uint256 priceWei);
    event DrawScheduled(uint256 indexed epochId, bytes32 commit, uint256 targetBlock);
    event DrawFinalized(uint256 indexed epochId, bytes32 entropy, uint256 minBuyWei);

    event WinnerDeclared(uint256 indexed epochId, address indexed winner);
    event WinnerChoseEth(uint256 indexed epochId, address indexed winner, uint256 amountWei);
    event WinnerChoseLock(uint256 indexed epochId, address indexed winner, uint256 punkId, uint256 unlockTs);

    event Forfeited(uint256 indexed epochId, address indexed winner);
    event Relisted(uint256 indexed epochId, uint256 punkId, uint256 priceWei);
    event RelistSold(uint256 indexed epochId, uint256 punkId, uint256 proceedsWei);

    event BurnStarted(uint256 amount);
    event BurnDripped(uint256 sent);

    event DisqualifiedGlobal(address indexed user);
    event DisqualifiedEpoch(uint256 indexed epochId, address indexed user);
    event MinBuySet(uint256 indexed epochId, uint256 minBuyWei);

    event BountyPaid(bytes32 indexed tag, address indexed to, uint256 amountWei);
    event V4ApprovalRevoked();
    event SwapTaxCredited(address indexed from, uint256 amountWei);

    // ===== Errors =====
    error NotOwner();
    error Paused();
    error SwapConfigMissing();
    error BurnZero();
    error PriorNotSettled();
    error Underfunded();

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

    modifier notPaused() {
        if (enginePaused) revert Paused();
        _;
    }

    // ===== Constructor =====
    constructor(
    address _punks,
    address _strategy,
    address payable _oracle,
    address _token,
    address _forwarder,
    address _dev,
    address _treasury
)
 {
        owner     = msg.sender;
        PUNKS     = ICryptoPunks(_punks);
        strategy  = IStrategyIndex(_strategy);
        oracle    = ListingOracle(_oracle);
        token     = PloopToken(_token);
        forwarder = V4Forwarder(payable(_forwarder));
        dev       = _dev;
        treasury  = _treasury;

        emit OwnerSet(owner);
        emit WiringSet(_punks, _strategy, _oracle, _token, _forwarder, _dev, _treasury);
    }

    // ===== Admin =====
    function setOwner(address o) external onlyOwner {
        owner = o;
        emit OwnerSet(o);
    }

    function setPaused(bool p) external onlyOwner {
        enginePaused = p;
        emit EnginePaused(p);
    }

    function setSwapsPaused(bool p) external onlyOwner {
        swapsPaused = p;
        emit SwapsPaused(p);
    }

    function setMinDripWei(uint256 w) external onlyOwner {
        minDripWei = w;
        emit MinDripWeiSet(w);
    }

    function setBurnDuration(uint40 secs) external onlyOwner {
        require(secs > 0, "DUR");
        burnDuration = secs;
        emit BurnDurationSet(secs);
    }

    function setMaxPoolAddrsPerEpoch(uint32 m) external onlyOwner {
        require(m > 0, "BAD_MAX");
        maxPoolAddrsPerEpoch = m;
        emit MaxPoolAddrsPerEpochSet(m);
    }

    function setEpochBountyLimit(uint256 limit) external onlyOwner {
        epochBountyLimit = limit;
    }

    function setBounties(uint256 reveal, uint256 resolve, uint256 confirm) external onlyOwner {
        bountyRevealWei  = reveal;
        bountyResolveWei = resolve;
        bountyConfirmWei = confirm;
    }

    function setAddresses(
    address _punks,
    address _strategy,
    address payable _oracle,
    address _forwarder,
    address _dev,
    address _treasury
) external onlyOwner {

        PUNKS     = ICryptoPunks(_punks);
        strategy  = IStrategyIndex(_strategy);
        oracle    = ListingOracle(_oracle);
        forwarder = V4Forwarder(payable(_forwarder));
        dev       = _dev;
        treasury  = _treasury;

        emit WiringSet(_punks, _strategy, _oracle, address(token), _forwarder, _dev, _treasury);
    }

    // ===== V4 wiring (two-step; can be done after Epoch 1) =====
    function setSwapConfig(address _router, address _pool, address _weth) external onlyOwner {
        require(!v4SwapLocked, "SWAP_LOCKED");
        require(_router != address(0) && _pool != address(0) && _weth != address(0), "ADDR0");

        address t0 = IUniswapV4PoolLite(_pool).token0();
        address t1 = IUniswapV4PoolLite(_pool).token1();
        require(
            (t0 == _weth && t1 == address(token)) || (t1 == _weth && t0 == address(token)),
            "POOL_PAIR"
        );

        v4Router = _router;
        v4Pool   = _pool;
        WETH     = _weth;

        // Infinite approve once for drip efficiency
        IWETH9(WETH).approve(v4Router, type(uint256).max);

        emit SwapConfigSet(_router, _pool, _weth, v4SwapLocked);
    }

    function lockSwapConfig() external onlyOwner {
        // Only lock after Epoch 1 has settled
        require(epochId >= 1 && epoch[1].settled, "E1_NOT_SETTLED");
        v4SwapLocked = true;
        emit SwapConfigSet(v4Router, v4Pool, WETH, v4SwapLocked);

        // Hygiene: optionally revoke after lock (router should no longer need new approvals)
        _revokeV4Approval();
    }

    function revokeV4Approval() external onlyOwner {
        _revokeV4Approval();
    }

    function _revokeV4Approval() internal {
        if (WETH != address(0) && v4Router != address(0)) {
            IWETH9(WETH).approve(v4Router, 0);
            emit V4ApprovalRevoked();
        }
    }

    // ===== Presale =====
    function startPresale() external onlyOwner notPaused {
        require(psStart == 0, "STARTED");
        psStart = uint40(block.timestamp);
        psEnd   = uint40(block.timestamp + PRESALE_DUR);
        emit PresaleStarted(psStart, psEnd);
    }

    function contribute() external payable notPaused {
        require(psStart != 0 && block.timestamp <= psEnd, "WINDOW");
        require(psTotal + msg.value <= PRESALE_HARD, "HARD");

        uint256 nb = psContrib[msg.sender] + msg.value;
        require(nb >= PRESALE_MIN && nb <= PRESALE_MAX, "RANGE");

        // 1.69% dev cut goes via forwarder (atomic credit)
        uint256 devCut = (msg.value * DEV_BIPS) / 10_000;
        (bool ok, ) = address(forwarder).call{value: devCut}(
            abi.encodeWithSignature(
                "creditFromEngine(address,address,uint256,uint256)",
                dev,
                treasury,
                devCut,
                0
            )
        );
        require(ok, "FWD");

        psContrib[msg.sender] = nb;
        psTotal += msg.value;

        emit PresaleContributed(msg.sender, msg.value);
    }

    function finalizePresale() external notPaused {
        require(block.timestamp > psEnd, "EARLY");
        require(psFinalized == 0, "FINAL");
        psFinalized = uint40(block.timestamp);

        if (psTotal >= PRESALE_SOFT) {
            psSucceeded = true;
            if (!tokensMinted) {
                token.setMinter(address(this));

                uint256 cap    = token.cap();
                uint256 toDev  = cap * DEV_BIPS / 10_000;
                uint256 toRest = cap - (cap * 60 / 100) - toDev; // remainder stays with engine (liquidity/burns/etc.)

                // Mint remainder to engine and dev to dev.
                token.mint(address(this), toRest);
                token.mint(dev, toDev);

                tokensMinted = true;
            }
        } else {
            psSucceeded = false;
        }

        emit PresaleFinalized(psSucceeded, psTotal);
    }

    function claimPresaleTokens() external notPaused {
        require(psSucceeded && psFinalized != 0, "PS");
        uint256 c = psContrib[msg.sender];
        require(c > 0 && !psClaimed[msg.sender], "CLAIMED");
        psClaimed[msg.sender] = true;

        uint256 presalePool = token.cap() * 60 / 100;

        // Direct mint per-claim
        uint256 due = (presalePool * c) / psTotal;
        token.mint(msg.sender, due);

        // Weight by ETH contributed (for presale slice)
        if (presaleWeight[msg.sender] == 0) presaleAddrs.push(msg.sender);
        presaleWeight[msg.sender] += c;

        emit PresaleClaimed(msg.sender, due, c);
    }

    // Optional batch claim helper (owner)
    function batchClaimPresale(address[] calldata users) external onlyOwner {
        require(psSucceeded && psFinalized != 0, "PS");
        uint256 presalePool = token.cap() * 60 / 100;
        for (uint256 i = 0; i < users.length; i++) {
            address u = users[i];
            uint256 c = psContrib[u];
            if (c == 0 || psClaimed[u]) continue;
            psClaimed[u] = true;

            uint256 due = (presalePool * c) / psTotal;
            token.mint(u, due);

            if (presaleWeight[u] == 0) presaleAddrs.push(u);
            presaleWeight[u] += c;

            emit PresaleClaimed(u, due, c);
        }
    }

    function claimRefund() external notPaused {
        require(psFinalized != 0 && !psSucceeded, "NOFAIL");
        uint256 amt = psContrib[msg.sender];
        require(amt > 0, "ZERO");
        psContrib[msg.sender] = 0;
        SafeTransferLib.safeTransferETH(msg.sender, amt);
        emit Refunded(msg.sender, amt);
    }

    // ===== Epoch purchase / RNG =====
    function _preferredPx() internal view returns (bool ok, uint256 id, uint256 px, address upd) {
        return oracle.preferredListing();
    }

    function _purchaseFromOracle() internal returns (uint256 punkId, uint256 priceWei) {
        (bool okL, uint256 id, uint256 px, ) = _preferredPx();
        require(okL, "NO_LISTING");

        // Underfunded buffer for buy + potential 20% ETH choice
        (, , uint256 px2, ) = _preferredPx();
        uint256 floorPx = px2 == 0 ? px : px2;
        uint256 twenty  = (floorPx * ETH_CHOICE_BIPS) / 10_000;
        uint256 required= px + twenty;
        if (address(this).balance < required) revert Underfunded();

        (bool ok, ) = address(PUNKS).call{value: px}(
            abi.encodeWithSelector(ICryptoPunks.buyPunk.selector, id)
        );
        require(ok, "PUNK_BUY_FAIL");

        // Start new epoch → reset per-epoch bounty spend
        epochId += 1;
        epochBountySpent[epochId] = 0;

        EpochState storage e = epoch[epochId];
        e.purchasedTs = uint40(block.timestamp);
        e.punkId = id;

        emit EpochPurchased(epochId, id, px);
        return (id, px);
    }

    function purchasePunk() external notPaused {
        if (epochId == 0) {
            require(psSucceeded, "PS");
            require(block.timestamp >= psFinalized + COOLDOWN, "COOLDOWN");
            _purchaseFromOracle();
        } else {
            // Require prior epoch settled (sale confirmed or locked)
            if (!epoch[epochId].settled) revert PriorNotSettled();
            _purchaseFromOracle();
        }
    }

    function scheduleDraw(bytes32 commit) external notPaused {
        EpochState storage e = epoch[epochId];
        require(e.purchasedTs != 0 && e.drawTargetBlock == 0, "STATE");
        e.saltCommit = commit;
        e.drawTargetBlock = uint40(block.number + 20);
        emit DrawScheduled(epochId, commit, e.drawTargetBlock);

        _payBounty("SCHEDULE", bountyRevealWei);
    }

    function finalizeDraw(bytes calldata salt) external notPaused {
        EpochState storage e = epoch[epochId];
        require(e.drawTargetBlock != 0 && block.number >= e.drawTargetBlock, "WAIT");
        require(e.entropy == 0, "DONE");
        require(keccak256(salt) == e.saltCommit, "COMMIT");

        bytes32 poolSnap = cumPoolHash;
        e.poolSnap = poolSnap;

        // Include epochId in entropy to avoid replay collisions
        bytes32 entropy = keccak256(
            abi.encodePacked(
                blockhash(e.drawTargetBlock),
                block.prevrandao,
                poolSnap,
                epochId,
                salt
            )
        );
        e.entropy = entropy;

        // Set next epoch's min buy randomly in [0.0169, 0.1069) ETH
        uint256 r = uint256(keccak256(abi.encode(entropy, "MINBUY")));
        uint256 minBuy = 16_900_000_000_000_000 + (r % (90_000_000_000_000_000));
        epoch[epochId + 1].minBuyWei = minBuy;

        emit MinBuySet(epochId + 1, minBuy);
        emit DrawFinalized(epochId, entropy, minBuy);

        _payBounty("FINALIZE", bountyRevealWei);

        resolveWinnerProgress(128);
    }

    function resolveWinnerProgress(uint256 steps) public notPaused {
        EpochState storage e = epoch[epochId];
        require(e.entropy != 0 && e.winner == address(0), "DONE");

        bool usePresale = (uint256(keccak256(abi.encode(e.entropy, "SLICE"))) % 10_000) < PRESALE_SLICE_BIPS
                          && epochId >= 2;

        address[] storage arr = usePresale ? presaleAddrs : poolAddrs[epochId];
        require(arr.length > 0, "EMPTY");

        uint256 n   = arr.length;
        uint256 idx = uint256(keccak256(abi.encode(e.entropy, "IDX"))) % n;
        uint256 step= 1 + (uint256(keccak256(abi.encode(e.entropy, "STEP"))) % (n - 1));

        uint256 cap = steps > MAX_RESOLVE_STEPS ? MAX_RESOLVE_STEPS : steps;

        for (uint256 i = 0; i < cap; i++) {
            address w = arr[idx];
            if (!(disqGlobal[w] || disqEpoch[epochId][w])) {
                e.winner = w;
                e.winnerDeclaredTs = uint40(block.timestamp);
                emit WinnerDeclared(epochId, w);

                _payBounty("RESOLVE", bountyResolveWei);
                return;
            }
            idx = (idx + step) % n;
        }
    }

    // ===== Winner choices =====
    function winnerChooseEth() external notPaused {
        EpochState storage e = epoch[epochId];
        require(msg.sender == e.winner && e.winnerDeclaredTs != 0, "WIN");
        require(block.timestamp <= e.winnerDeclaredTs + 24 hours, "EXPIRED");

        (bool ok, , uint256 floorPx, ) = _preferredPx();
        require(ok, "NO_FLOOR");

        uint256 twenty = (floorPx * ETH_CHOICE_BIPS) / 10_000;
        uint256 devCut = (twenty * DEV_BIPS) / 10_000;

        (bool okc, ) = address(forwarder).call{value: devCut}(
            abi.encodeWithSignature(
                "creditFromEngine(address,address,uint256,uint256)",
                dev,
                treasury,
                devCut,
                0
            )
        );
        require(okc, "FWD");

        SafeTransferLib.safeTransferETH(msg.sender, twenty - devCut);
        emit WinnerChoseEth(epochId, msg.sender, twenty);

        // Relist at current floor
        PUNKS.offerPunkForSale(e.punkId, floorPx);
        e.relisted = true;
        e.relistFloorWei = floorPx;
        e.lastRelistUpdateTs = uint40(block.timestamp);
        emit Relisted(epochId, e.punkId, floorPx);

        _payBounty("WIN_ETH", bountyConfirmWei);
    }

    function winnerChooseLock(uint256 punkId) external notPaused {
        EpochState storage e = epoch[epochId];
        require(msg.sender == e.winner && e.winnerDeclaredTs != 0, "WIN");
        require(block.timestamp <= e.winnerDeclaredTs + 24 hours, "EXPIRED");
        require(punkId == e.punkId, "PUNK");

        uint40 unlockTs = uint40(block.timestamp + 365 days);
        locked[msg.sender] = Lock({ punkId: punkId, unlockTs: unlockTs });
        emit WinnerChoseLock(epochId, msg.sender, punkId, unlockTs);

        e.settled = true;
        _payBounty("WIN_LOCK", bountyConfirmWei);
    }

    function claimPunk() external notPaused {
        Lock memory L = locked[msg.sender];
        require(L.punkId != 0 && block.timestamp >= L.unlockTs, "LOCK");
        locked[msg.sender] = Lock(0, 0);
        PUNKS.transferPunk(msg.sender, L.punkId);
    }

    function forfeitIfExpired() external notPaused {
        EpochState storage e = epoch[epochId];
        require(e.winner != address(0) && block.timestamp > e.winnerDeclaredTs + 24 hours, "NO");

        emit Forfeited(epochId, e.winner);

        (bool ok, , uint256 floorPx, ) = _preferredPx();
        require(ok, "NO_FLOOR");

        if (e.relisted) {
            require(block.timestamp >= e.lastRelistUpdateTs + 1 hours, "COOLDOWN");
        }

        PUNKS.offerPunkForSale(e.punkId, floorPx);
        e.relisted = true;
        e.relistFloorWei = floorPx;
        e.lastRelistUpdateTs = uint40(block.timestamp);
        emit Relisted(epochId, e.punkId, floorPx);

        _payBounty("FORFEIT", bountyConfirmWei);
    }

    function updateRelistToFloor() external notPaused {
        EpochState storage e = epoch[epochId];
        require(e.relisted && !e.settled, "STATE");
        require(block.timestamp >= e.lastRelistUpdateTs + 1 hours, "COOLDOWN");

        (bool ok, , uint256 floorPx, ) = _preferredPx();
        require(ok, "NO_FLOOR");

        PUNKS.offerPunkForSale(e.punkId, floorPx);
        e.relistFloorWei = floorPx;
        e.lastRelistUpdateTs = uint40(block.timestamp);
        emit Relisted(epochId, e.punkId, floorPx);
    }

    // Relist sale settlement (payable; stores ETH for later drips)
    function confirmPunkSale(uint256 punkId, uint256 proceedsWei) external payable notPaused nonReentrantConfirm {
        EpochState storage e = epoch[epochId];
        require(e.relisted && !e.settled && punkId == e.punkId, "STATE");

        if (e.relistFloorWei != 0) {
            require(proceedsWei >= (e.relistFloorWei * 95) / 100, "LOW_PROCEEDS");
        }

        // Ensure the ETH being accounted for is actually sent to this contract
        require(msg.value == proceedsWei, "VALUE_MISMATCH");

        // Schedule burn stream in ETH; swaps may occur later when V4 is configured
        uint256 burnAmt = (proceedsWei * SELL_BURN_BIPS) / 100_000;
        burn = Burn({
            amount: burnAmt,
            start: uint40(block.timestamp),
            duration: uint40(burnDuration),
            remaining: burnAmt
        });
        emit BurnStarted(burnAmt);

        // Dev cut goes out immediately (1.69% of proceeds)
        uint256 devCut = (proceedsWei * DEV_BIPS) / 10_000;
        (bool okc, ) = address(forwarder).call{value: devCut}(
            abi.encodeWithSignature(
                "creditFromEngine(address,address,uint256,uint256)",
                dev,
                treasury,
                devCut,
                0
            )
        );
        require(okc, "FWD");

        emit RelistSold(epochId, punkId, proceedsWei);
        e.settled = true;

        _payBounty("CONFIRM", bountyConfirmWei);
    }

    // ===== Swap-tax credit sink (for hook/forwarder) =====
    function creditSwapTax() external payable {
        emit SwapTaxCredited(msg.sender, msg.value);
        // Intentionally no logic — ETH stays to fund burns/bounties/purchases
    }

    // ===== V4 swap/burn path =====
    function _buyPloopViaV4AndBurn(uint256 ethIn, uint256 minOut) internal {
        if (ethIn == 0) revert BurnZero();

        // Skip if contract does not hold enough ETH yet (e.g., tiny due)
        if (address(this).balance < ethIn) return;

        if (v4Router == address(0) || v4Pool == address(0) || WETH == address(0)) {
            revert SwapConfigMissing();
        }
        require(!swapsPaused, "SWAPS_PAUSED");

        // Wrap and swap
        IWETH9(WETH).deposit{value: ethIn}();

        bytes memory hook = abi.encode(v4Pool);
        IUniswapV4Router.ExactInputSingleParams memory p =
            IUniswapV4Router.ExactInputSingleParams({
                tokenIn: WETH,
                tokenOut: address(token),
                amountIn: ethIn,
                amountOutMinimum: minOut,
                recipient: address(this),
                sqrtPriceLimitX96: 0,
                hookData: hook
            });

        uint256 out = IUniswapV4Router(v4Router).exactInputSingle(p);
        require(out > 0, "NO_OUT");

        token.burn(out);
    }

    // If V4 isn’t configured yet, dripBurn simply returns (defers swaps)
    function dripBurn(uint256 minOut) external notPaused {
        Burn storage b = burn;
        require(b.remaining > 0, "NONE");

        uint256 elapsed = block.timestamp > b.start ? (block.timestamp - b.start) : 0;
        if (elapsed > b.duration) elapsed = b.duration;

        uint256 due   = (b.amount * elapsed) / b.duration;
        uint256 spent = b.amount - b.remaining;

        if (due > spent) {
            uint256 toSwap = due - spent;

            // If V4 is not set or swaps are paused, just defer
            if (v4Router == address(0) || v4Pool == address(0) || WETH == address(0) || swapsPaused) {
                return;
            }

            if (minDripWei != 0 && toSwap < minDripWei) return;

            _buyPloopViaV4AndBurn(toSwap, minOut);
            b.remaining -= toSwap;

            emit BurnDripped(toSwap);
        }
    }

    // ===== Pool eligibility hooks =====
    function onPoolBuy(address buyer, uint256 amountWei) external notPaused {
        EpochState storage e = epoch[epochId];
        if (amountWei >= e.minBuyWei && !seenPool[epochId][buyer]) {
            if (poolAddrs[epochId].length < maxPoolAddrsPerEpoch) {
                poolAddrs[epochId].push(buyer);
            }
            seenPool[epochId][buyer] = true;
            cumPoolHash = keccak256(abi.encode(cumPoolHash, buyer, amountWei, block.number));
        }
    }

    function batchAccruePool(address[] calldata buyers, uint256[] calldata amountsWei)
        external
        notPaused
    {
        require(buyers.length == amountsWei.length, "LEN");
        EpochState storage e = epoch[epochId];

        for (uint256 i = 0; i < buyers.length; i++) {
            if (amountsWei[i] >= e.minBuyWei && !seenPool[epochId][buyers[i]]) {
                if (poolAddrs[epochId].length < maxPoolAddrsPerEpoch) {
                    poolAddrs[epochId].push(buyers[i]);
                }
                seenPool[epochId][buyers[i]] = true;
                cumPoolHash = keccak256(abi.encode(cumPoolHash, buyers[i], amountsWei[i], block.number));
            }
        }
    }

    function onPoolSell(address seller, bool inV4) external notPaused {
        if (!inV4) {
            if (psContrib[seller] > 0) {
                disqGlobal[seller] = true;
                emit DisqualifiedGlobal(seller);
            }
            disqEpoch[epochId][seller] = true;
            emit DisqualifiedEpoch(epochId, seller);
        }
    }

    // ===== Bounties =====
    function _payBounty(bytes32 tag, uint256 amt) internal {
        if (amt == 0) return;
        if (amt > MAX_BOUNTY_PER_TX_WEI) amt = MAX_BOUNTY_PER_TX_WEI;
        if (address(this).balance < amt) return; // best effort

        // Enforce per-epoch cap
        uint256 spent = epochBountySpent[epochId];
        if (spent + amt > epochBountyLimit) return;
        epochBountySpent[epochId] = spent + amt;

        SafeTransferLib.safeTransferETH(msg.sender, amt);
        emit BountyPaid(tag, msg.sender, amt);
    }

    // ===== Receive ETH =====
    receive() external payable {}
}
"
    },
    "contracts/interfaces/ICryptoPunks.sol": {
      "content": "
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface ICryptoPunks {
    function buyPunk(uint256 punkIndex) external payable;
    function transferPunk(address to, uint256 punkIndex) external;
    function offerPunkForSale(uint256 punkIndex, uint256 minSalePriceInWei) external;
}
"
    },
    "contracts/interfaces/IStrategyIndex.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IStrategyIndex {
    /// Buy a Punk and relist it on-chain.
    function buyPunkAndRelist(uint256 punkId) external payable;

    /// Return the global floor price (in wei) for listed Punks.
    function globalFloor() external view returns (uint256);

    /// (Optional) Return the listing price for a specific Punk.
    function getPunkPrice(uint256 punkId) external view returns (uint256);
}
"
    },
    "contracts/interfaces/IWETH9.sol": {
      "content": "
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IWETH9 {
    function deposit() external payable;
    function withdraw(uint256) external;
    function approve(address guy, uint256 wad) external returns (bool);
}
"
    },
    "contracts/interfaces/IUniswapV4Router.sol": {
      "content": "
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IUniswapV4Router {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint256 amountIn;
        uint256 amountOutMinimum;
        address recipient;
        uint160 sqrtPriceLimitX96;
        bytes hookData;
    }
    function exactInputSingle(ExactInputSingleParams calldata) external payable returns (uint256 amountOut);
}
"
    },
    "contracts/interfaces/IUniswapV4PoolLite.sol": {
      "content": "
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IUniswapV4PoolLite {
    function token0() external view returns (address);
    function token1() external view returns (address);
}
"
    },
    "contracts/ListingOracle.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./interfaces/IStrategyIndex.sol";

/**
 * ListingOracle v1.9.2 (Mainnet-Ready)
 * ------------------------------------
 * - Integrates with external StrategyIndex for PunkLoopEngine.
 * - Tracks and updates the on-chain floor.
 * - Owner-tunable safety parameters.
 * - Optional updater whitelist (approveUpdater) for ConfigAll.s.sol compatibility.
 */

contract ListingOracle {
    address public owner;
    IStrategyIndex public strategy;

    // ---- Last observation ----
    uint256 public lastUpdateTs;
    uint256 public lastFloorWei;
    uint256 public lastSpreadBips;
    uint256 public lastListings;

    // ---- Tunables ----
    uint256 public floorCooldown = 600;   // seconds between updates
    uint256 public maxSpreadBips = 2000;  // 20% tolerance
    uint256 public retryDelay    = 60;    // retry delay on failure
    uint256 public minListings   = 15;    // min listings for validity

    // ---- Updater whitelist ----
    mapping(address => bool) public approvedUpdater;
    bool public whitelistMode;  // if false, anyone can call refreshFloor()

    // ---- Events ----
    event OwnerSet(address indexed owner);
    event StrategySet(address indexed strategy);
    event TunablesSet(uint256 floorCooldown, uint256 maxSpreadBips, uint256 retryDelay, uint256 minListings);
    event FloorUpdated(uint256 floorWei, uint256 listings, uint256 spreadBips);
    event UpdaterApproved(address indexed updater, bool approved);
    event WhitelistModeSet(bool enabled);

    // ---- Errors ----
    error NotOwner();
    error CooldownActive();
    error InvalidResponse();
    error TooFewListings();
    error NotApproved();

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

    modifier onlyUpdater() {
        if (whitelistMode && !approvedUpdater[msg.sender] && msg.sender != owner) revert NotApproved();
        _;
    }

    constructor(address _strategy) {
        require(_strategy != address(0), "ADDR0");
        owner = msg.sender;
        strategy = IStrategyIndex(_strategy);
        emit OwnerSet(owner);
        emit StrategySet(_strategy);
    }

    // ---- Owner admin ----
    function setOwner(address _owner) external onlyOwner {
        require(_owner != address(0), "ADDR0");
        owner = _owner;
        emit OwnerSet(_owner);
    }

    function setStrategy(address _strategy) external onlyOwner {
        require(_strategy != address(0), "ADDR0");
        strategy = IStrategyIndex(_strategy);
        emit StrategySet(_strategy);
    }

    function setTunables(
        uint256 _floorCooldown,
        uint256 _maxSpreadBips,
        uint256 _retryDelay,
        uint256 _minListings
    ) external onlyOwner {
        require(_floorCooldown >= 60 && _retryDelay >= 30, "LOW_INTERVAL");
        require(_maxSpreadBips <= 10_000, "BIPS");
        floorCooldown = _floorCooldown;
        maxSpreadBips = _maxSpreadBips;
        retryDelay    = _retryDelay;
        minListings   = _minListings;
        emit TunablesSet(_floorCooldown, _maxSpreadBips, _retryDelay, _minListings);
    }

    // ---- Updater whitelist management ----
    function approveUpdater(address updater, bool approved) external onlyOwner {
        approvedUpdater[updater] = approved;
        emit UpdaterApproved(updater, approved);
    }

    function setWhitelistMode(bool enabled) external onlyOwner {
        whitelistMode = enabled;
        emit WhitelistModeSet(enabled);
    }

    // ---- Public view helpers ----
    function latestFloor() external view returns (uint256) {
        return lastFloorWei;
    }

    function preferredListing()
        external
        view
        returns (bool ok, uint256 punkId, uint256 priceWei, address updater)
    {
        try strategy.globalFloor() returns (uint256 floor) {
            return (floor > 0, 0, floor, address(strategy));
        } catch {
            return (false, 0, 0, address(strategy));
        }
    }

    // ---- Active updater ----
    function refreshFloor() external onlyUpdater {
        uint256 nowTs = block.timestamp;
        if (nowTs < lastUpdateTs + floorCooldown) revert CooldownActive();

        (bool ok, , uint256 newFloor, ) = this.preferredListing();
        if (!ok || newFloor == 0) revert InvalidResponse();

        uint256 prevFloor = lastFloorWei;
        if (prevFloor > 0) {
            uint256 spread = prevFloor > newFloor
                ? ((prevFloor - newFloor) * 10_000) / prevFloor
                : ((newFloor - prevFloor) * 10_000) / prevFloor;
            if (spread > maxSpreadBips) revert InvalidResponse();
            lastSpreadBips = spread;
        }

        // Placeholder: assume listings >= minListings for trusted oracle feed
        uint256 listings = minListings;
        if (listings < minListings) revert TooFewListings();

        lastUpdateTs = nowTs;
        lastFloorWei = newFloor;
        lastListings = listings;

        emit FloorUpdated(newFloor, listings, lastSpreadBips);
    }

    receive() external payable {}
}
"
    },
    "contracts/PloopToken.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/// @title PloopToken (capped ERC20) — v1.6
/// @notice Hard-capped at 1,000,000,000 PLOOP. Single minter (set by owner).
///         Engine expects: cap(), setMinter(address), mint(to,amount), burn(amount),
///         standard ERC20 (transfer/transferFrom/approve).
contract PloopToken {
    // ============ ERC20 Storage ============
    string public name;
    string public symbol;
    uint8  public constant decimals = 18;

    uint256 public totalSupply;
    mapping(address => uint256)                       public balanceOf;
    mapping(address => mapping(address => uint256))   public allowance;

    // ============ Cap / Roles ============
    uint256 public constant CAP_TOKENS = 1_000_000_000 ether; // 1B * 1e18

    address public owner;
    address public minter;

    // ============ Events ============
    event Transfer(address indexed from, address indexed to, uint256 amount);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    event OwnerSet(address indexed newOwner);
    event MinterSet(address indexed newMinter);

    // ============ Errors ============
    error NotOwner();
    error NotMinter();
    error CapExceeded();

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

    modifier onlyMinter() {
        if (msg.sender != minter) revert NotMinter();
        _;
    }

    // ============ Constructor ============
    /// @param _name   ERC20 name (e.g., "Ploop Token")
    /// @param _symbol ERC20 symbol (e.g., "PLOOP")
    /// @param _owner  Initial owner (can set/rotate minter)
    constructor(string memory _name, string memory _symbol, address _owner) {
        name  = _name;
        symbol= _symbol;
        owner = _owner;
        emit OwnerSet(_owner);
    }

    // ============ View Helpers ============
    function cap() external pure returns (uint256) {
        return CAP_TOKENS;
    }

    // ============ Owner Controls ============
    function setOwner(address newOwner) external onlyOwner {
        owner = newOwner;
        emit OwnerSet(newOwner);
    }

    /// @notice Set (or rotate) the single minter. Expected to be the Engine.
    function setMinter(address newMinter) external onlyOwner {
        minter = newMinter;
        emit MinterSet(newMinter);
    }

    // ============ Mint / Burn ============
    /// @notice Mint new tokens up to the hard cap.
    function mint(address to, uint256 amount) external onlyMinter {
        uint256 newTotal = totalSupply + amount;
        if (newTotal > CAP_TOKENS) revert CapExceeded();

        totalSupply = newTotal;
        unchecked {
            balanceOf[to] += amount;
        }
        emit Transfer(address(0), to, amount);
    }

    /// @notice Burn caller's tokens (Engine or users can burn their own balance).
    function burn(uint256 amount) external {
        uint256 bal = balanceOf[msg.sender];
        require(bal >= amount, "INSUFFICIENT_BALANCE");
        unchecked {
            balanceOf[msg.sender] = bal - amount;
            totalSupply -= amount;
        }
        emit Transfer(msg.sender, address(0), amount);
    }

    // ============ ERC20 ============
    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transfer(address to, uint256 amount) external returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external returns (bool) {
        uint256 allowed = allowance[from][msg.sender];
        if (allowed != type(uint256).max) {
            require(allowed >= amount, "ALLOWANCE");
            unchecked {
                allowance[from][msg.sender] = allowed - amount;
            }
            emit Approval(from, msg.sender, allowance[from][msg.sender]);
        }
        _transfer(from, to, amount);
        return true;
    }

    // ============ Internal ============
    function _transfer(address from, address to, uint256 amount) internal {
        require(to != address(0), "ZERO_TO");
        uint256 bal = balanceOf[from];
        require(bal >= amount, "BALANCE");
        unchecked {
            balanceOf[from] = bal - amount;
            balanceOf[to] += amount;
        }
        emit Transfer(from, to, amount);
    }
}
"
    },
    "contracts/V4Forwarder.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./libraries/SafeTransferLib.sol";

/**
 * @title V4Forwarder
 * @notice Unified Forwarder + Uniswap V4 "hook sink" helper
 * - Splits engine-directed ETH (dev 1.69% / treasury remainder)
 * - Applies swap tax for a V4 pool and forwards to dev/treasury
 * - NEW: paySwapTax(engine) forwards msg.value to engine.creditSwapTax()
 */
contract V4Forwarder {
    using SafeTransferLib for address payable;

    // ---- Admin ----
    address public owner;
    address payable public dev;
    address payable public treasury;

    // 1.69% to dev on credits / taxes
    uint16 public constant DEV_BIPS = 169;

    // ---- V4 Hook Config ----
    address public weth;
    address public ploop;
    address public pool;
    bool    public hookEnabled;

    // % tax (in bips) to levy on pool swaps (default 10%)
    uint16 public taxBips = 1000;

    // ---- Simple Reentrancy Guard ----
    bool private _entered;
    modifier nonReentrant() {
        require(!_entered, "REENTRANT");
        _entered = true;
        _;
        _entered = false;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "NOT_OWNER");
        _;
    }

    // ---- Events ----
    event OwnerSet(address indexed newOwner);
    event DevSet(address indexed newDev);
    event TreasurySet(address indexed newTreasury);
    event TaxBipsSet(uint16 newBips);
    event HookConfigured(address weth, address ploop, address pool);
    event HookToggle(bool enabled);
    event CreditForwarded(address indexed from, uint256 devAmt, uint256 treasAmt);
    event SwapTaxApplied(address indexed sender, uint256 taxWei, uint256 toTreasury, uint256 toDev);
    event SwapTaxForwarded(address indexed engine, uint256 amountWei);

    // ---- Constructor ----
    constructor(address _owner) {
        owner = _owner;
        emit OwnerSet(_owner);
    }

    // ---- Owner functions ----
    function setOwner(address _owner) external onlyOwner {
        owner = _owner;
        emit OwnerSet(_owner);
    }

    function setDev(address payable _dev) external onlyOwner {
        dev = _dev;
        emit DevSet(_dev);
    }

    function setTreasury(address payable _treasury) external onlyOwner {
        treasury = _treasury;
        emit TreasurySet(_treasury);
    }

    function setTaxBips(uint16 b) external onlyOwner {
        require(b <= 2000, "TOO_HIGH");
        taxBips = b;
        emit TaxBipsSet(b);
    }

    function configureHook(address _weth, address _ploop, address _pool) external onlyOwner {
        weth = _weth;
        ploop = _ploop;
        pool = _pool;
        hookEnabled = true;
        emit HookConfigured(_weth, _ploop, _pool);
    }

    function toggleHook(bool e) external onlyOwner {
        hookEnabled = e;
        emit HookToggle(e);
    }

    // ======================================================
    //  ENGINE CREDIT PATHS (for presales / relist / ETH choice)
    // ======================================================

    /**
     * @notice Receives value from PunkLoopEngine and splits between dev/treasury
     * @dev msg.value must equal devAmt + treasuryAmt
     */
    function creditFromEngine(
        address _dev,
        address _treasury,
        uint256 devAmt,
        uint256 treasuryAmt
    ) external payable nonReentrant {
        require(msg.value == devAmt + treasuryAmt, "BAD_VALUE");
        if (devAmt > 0) payable(_dev).safeTransferETH(devAmt);
        if (treasuryAmt > 0) payable(_treasury).safeTransferETH(treasuryAmt);
        emit CreditForwarded(msg.sender, devAmt, treasuryAmt);
    }

    // ======================================================
    //  UNISWAP V4 HOOK TAX LOGIC
    // ======================================================

    /**
     * @notice Called automatically by a Uniswap V4 pool during swap execution.
     * @dev Must be set as the pool's hook recipient (off-chain integration).
     *      Applies a fixed % tax on swaps (10% default),
     *      then forwards the split (1.69% dev / rest treasury).
     */
    function applySwapTax() external payable nonReentrant {
        require(hookEnabled, "HOOK_OFF");
        require(msg.sender == pool, "NOT_POOL");
        require(msg.value > 0, "NO_VALUE");

        uint256 tax = (msg.value * taxBips) / 10_000;
        if (tax == 0) return;

        uint256 devCut = (tax * DEV_BIPS) / 10_000;
        uint256 treasCut = tax - devCut;

        if (devCut > 0) dev.safeTransferETH(devCut);
        if (treasCut > 0) treasury.safeTransferETH(treasCut);

        emit SwapTaxApplied(msg.sender, tax, treasCut, devCut);
    }

    // ======================================================
    //  NEW: Forward swap-tax ETH directly to the Engine
    // ======================================================

    /**
     * @notice Convenience sink for hooks/keepers:
     *         forwards msg.value to engine.creditSwapTax().
     * @param engine Address of PunkLoopEngine
     */
    function paySwapTax(address engine) external payable nonReentrant {
        require(engine != address(0), "ENGINE_0");
        // Forward all ETH to the engine's creditSwapTax()
        (bool ok, ) = engine.call{value: msg.value}(abi.encodeWithSignature("creditSwapTax()"));
        require(ok, "ENGINE_FORWARD_FAIL");
        emit SwapTaxForwarded(engine, msg.value);
    }

    // ======================================================
    //  FALLBACKS
    // ======================================================
    receive() external payable {}
    fallback() external payable {}
}
"
    },
    "contracts/libraries/SafeTransferLib.sol": {
      "content": "
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

library SafeTransferLib {
    function safeTransferETH(address to, uint256 amount) internal {
        (bool ok, ) = to.call{value: amount}("");
        require(ok, "ETH_TRANSFER_FAIL");
    }
}
"
    }
  },
  "settings": {
    "remappings": [
      "forge-std/=lib/forge-std/src/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 400
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "cancun",
    "viaIR": true
  }
}}

Tags:
Multisig, Swap, Liquidity, Multi-Signature, Factory, Oracle|addr:0x2140c8eff891b7ea232021995fb56e440adccc29|verified:true|block:23683612|tx:0x0ee71f086d4f6b6407ca2720ba439f7d4453b0166b29ebc397420486ec538b6f|first_check:1761754945

Submitted on: 2025-10-29 17:22:25

Comments

Log in to comment.

No comments yet.