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
}
}}
Submitted on: 2025-10-29 17:22:25
Comments
Log in to comment.
No comments yet.