// SPDX-License-Identifier: MIT pragma solidity 0.8.30; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; import {IUnlockCallback} from "v4-core/interfaces/callback/IUnlockCallback.sol"; import {IHooks} from "v4-core/interfaces/IHooks.sol"; import {PoolKey} from "v4-core/types/PoolKey.sol"; import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol"; import {SwapParams} from "v4-core/types/PoolOperation.sol"; import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; import {Currency} from "v4-core/types/Currency.sol"; import {TickMath} from "v4-core/libraries/TickMath.sol"; import {FullMath} from "v4-core/libraries/FullMath.sol"; import {StateLibrary} from "v4-core/libraries/StateLibrary.sol"; import { GraduationPhase, LaunchedToken, IPonsFactory, IPonsCurve, IPonsHook, IPonsFeeEscrow } from "./interfaces/IPons.sol"; /// The Grand game's side of a lock: told about every lock on a joined sign, it decides whether it counts. interface IGrandGame { function recordLock(address locker, uint256 value) external; } /// The shared referral list's side of a Buy & lock (Referrals.sol). interface IReferrals { function recordBuy(address buyer, address referrer, uint256 reward) external returns (uint256 credited); function fund() external payable; } /// @title SignGame /// @notice One zodiac sign's "last to lock wins" game on top of its own ETH-paired Pons token. /// /// The game is the token's Pons creator fee recipient. Every fee payment it collects from Pons is split 40% into /// this sign's pot, 10% to the Grand game (held here until one is connected) and 50% to the sign's team wallet: the /// team keeps half, and a fifth of the game's half goes on to the Grand as it arrives. /// /// To enter, a holder locks the sign's token here for `lockLength`, worth at least `minLockNow()` in ETH (5% of the /// pot at launch settings). Two ways: send ETH and the game buys the token and locks it (`buyAndLock`), or lock /// tokens already held (`lockTokens`), valued at what they would sell for right now after fees. Every lock puts /// the clock back to its full length, and locking more than the minimum is fine. When the clock runs out, the last /// to lock wins half the pot, sent straight to them, and the other half stays as the next round's pot. Locked tokens /// go back to their owner after `lockLength`. /// /// When a Grand game is connected, every lock is also reported to it; a failing Grand never blocks a lock here. /// /// Referrals: every Buy & lock is reported to the shared referral list with the link the buyer came from. If the /// buyer has referrers there, 20% of the team's share of that buy's fees is credited to them (70% to the direct /// referrer, 30% to theirs) and paid over from the team's share as the fees come in. Nothing else of the fees is /// touched, and a failing referral list never blocks a lock. /// /// The game can be deployed before its token exists: launch the token on Pons with this game as its creator fee /// recipient, then the sign's wallet calls `setToken` once. Until then the game takes no locks. contract SignGame is ReentrancyGuard, IUnlockCallback { using SafeERC20 for IERC20; using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; uint256 private constant BPS = 10_000; uint256 private constant Q96 = 2 ** 96; /// Shares of every fee payment from Pons. The team gets what is left (50%). uint256 public constant POT_BPS = 4_000; uint256 public constant GRAND_BPS = 1_000; /// The referrers' cut of the team's share of a referred Buy & lock's fees. uint256 public constant REFERRAL_BPS = 2_000; /// The winner's share of the pot when a clock runs out. The rest (50%) stays for the next round. uint256 public constant WINNER_BPS = 5_000; /// Notice the team must give before the Pons fee role can move to another address. uint256 public constant FEE_ROLE_DELAY = 7 days; /// Gas the Grand game gets to record a lock; anything it does beyond that cannot hold a lock up. uint256 public constant GRAND_GAS = 300_000; /// @notice Gas given to a winner's wallet when its prize is sent. A wallet that refuses the ETH, or tries to burn /// the caller's gas, cannot hold up the game; it keeps the prize and claims it itself. uint256 public constant PAYOUT_GAS = 60_000; /// Gas the referral list gets to record a Buy & lock; like the Grand, it can never hold a lock up. uint256 public constant REFERRAL_GAS = 200_000; struct Settings { uint32 clockLength; // seconds the clock runs after each lock uint32 lockLength; // seconds each lock's tokens stay locked uint128 minLock; // smallest lock, in wei of value uint16 minLockPotBps; // a lock must also be worth at least this share of the pot uint128 startFloor; // the clock only starts once the pot holds at least this much } struct Round { address winner; uint256 winnerPrize; } struct Lock { address owner; uint64 unlockAt; bool withdrawn; uint256 amount; } IPonsFactory public immutable factory; /// This sign's Pons token and its bonding curve: set at deployment, or once later with `setToken`. address public token; address public curve; address public immutable hook; IPoolManager public immutable poolManager; IPonsFeeEscrow public immutable feeEscrow; /// This sign's own wallet: receives the team share and holds the two team switches. address public immutable team; /// The referral list shared by every sign, or zero for none. address public immutable referrals; uint256 public immutable clockLength; uint256 public immutable lockLength; uint256 public immutable minLock; uint256 public immutable minLockPotBps; uint256 public immutable startFloor; /// ETH waiting to be won in the current round. uint256 public pot; /// ETH set aside for the Grand game until one is connected. uint256 public grandSavings; /// Prizes and round shares won but not yet claimed. uint256 public owed; mapping(address => uint256) public prizeOf; uint256 public round = 1; address public lastLocker; uint256 public lastLockAt; /// When the current clock runs out. Zero while the clock is not running. uint256 public deadline; mapping(uint256 => Round) public rounds; Lock[] public locks; uint256 public totalLocked; address public grand; /// Referral rewards already credited to referrers and still to be paid over from the team's share of fees. uint256 public referralDebt; address public pendingFeeRecipient; uint256 public feeRecipientReadyAt; event Locked( uint256 indexed round, address indexed locker, uint256 value, uint256 tokens, uint256 lockId, uint256 deadline, bool bought ); event FeesCollected(uint256 amount, uint256 toPot, uint256 toGrand, uint256 toTeam); event RoundSettled(uint256 indexed round, address indexed winner, uint256 winnerPrize, uint256 potLeft); event Claimed(address indexed locker, uint256 amount); event PaidOut(address indexed winner, uint256 amount); event Unlocked(address indexed locker, uint256 amount); event Donated(address indexed from, uint256 amount); event GrandConnected(address grand); event TokenSet(address token, address curve); event GrandFunded(uint256 amount); event ReferralsFunded(uint256 amount); event FeeRecipientProposed(address next, uint256 readyAt); event FeeRecipientCancelled(); event FeeRecipientMoved(address next); error NotTeam(); error NotEthPair(); error UnknownToken(); error NoTokenYet(); error TokenAlreadySet(); error NotOurToken(); error BadSettings(); error LockTooSmall(uint256 value, uint256 minimum); error NotTrading(); error ClockRunning(); error NothingToClaim(); error NotYourLock(uint256 lockId); error StillLocked(uint256 lockId, uint256 unlockAt); error AlreadyWithdrawn(uint256 lockId); error GrandAlreadyConnected(); error ZeroAddress(); error NoPendingFeeRecipient(); error FeeRecipientNotReady(uint256 readyAt); error NotPoolManager(); error TooLittleReceived(uint256 got, uint256 minimum); error SendFailed(); modifier withToken() { if (token == address(0)) revert NoTokenYet(); _; } modifier onlyTeam() { if (msg.sender != team) revert NotTeam(); _; } /// @param grand_ The Grand game to report locks to and fund, or zero to connect one later. /// @param referrals_ The shared referral list, or zero for none (it can't be added later). constructor( IPonsFactory factory_, address token_, address team_, address grand_, address referrals_, Settings memory settings ) { if (team_ == address(0)) revert ZeroAddress(); if (settings.clockLength == 0 || settings.lockLength == 0 || settings.minLockPotBps > BPS) revert BadSettings(); factory = factory_; if (token_ != address(0)) { LaunchedToken memory launch = factory_.getLaunchedToken(token_); if (!launch.exists) revert UnknownToken(); if (launch.pairToken != address(0)) revert NotEthPair(); token = token_; curve = launch.curve; emit TokenSet(token_, launch.curve); } hook = factory_.memeHook(); poolManager = IPoolManager(factory_.poolManager()); feeEscrow = IPonsFeeEscrow(factory_.feeEscrow()); team = team_; grand = grand_; referrals = referrals_; clockLength = settings.clockLength; lockLength = settings.lockLength; minLock = settings.minLock; minLockPotBps = settings.minLockPotBps; startFloor = settings.startFloor; } // --------------------------------------------------------------------- // Locking // --------------------------------------------------------------------- /// @notice Buy this sign's token with the ETH sent and lock it, in one step. /// @param minTokensOut Slippage guard on the buy. /// @param referrer The referral link the buyer came from, or zero. It only counts on the buyer's first Buy & lock. function buyAndLock(uint256 minTokensOut, address referrer) external payable nonReentrant withToken returns (uint256 lockId) { uint256 minimum = _openRound(); (uint256 spent, uint256 tokensOut, uint256 fee) = _buy(msg.value, minTokensOut); if (spent < minimum) revert LockTooSmall(spent, minimum); lockId = _enter(msg.sender, spent, tokensOut, true); _reward(msg.sender, referrer, fee); _collect(); // A buy that crosses the graduation line can fill only part of the ETH. if (spent < msg.value) _send(msg.sender, msg.value - spent); } /// @notice Lock tokens already held. They count at what they would sell for right now, after fees. function lockTokens(uint256 amount) external nonReentrant withToken returns (uint256 lockId) { uint256 minimum = _openRound(); uint256 value = valueOf(amount); if (value < minimum) revert LockTooSmall(value, minimum); IERC20(token).safeTransferFrom(msg.sender, address(this), amount); lockId = _enter(msg.sender, value, amount, false); _collect(); } /// @notice End the round once its clock has run out and send the winner their prize. Anyone may call it; the /// next lock also does. function settle() external nonReentrant { if (deadline == 0 || block.timestamp < deadline) revert ClockRunning(); _collect(); _pay(_settle()); } /// @notice Send `winner` every prize they have won and not yet received. Anyone may call it, so a win never waits /// on the winner; if the clock has run out it settles that round first. Round shares stay with claim(). function payOut(address winner) external nonReentrant returns (uint256 amount) { if (deadline != 0 && block.timestamp >= deadline) { _collect(); _settle(); } amount = _pay(winner); if (amount == 0) revert NothingToClaim(); } /// @notice Pull this sign's fees out of Pons and split them. Anyone may call it. function collect() external nonReentrant withToken returns (uint256 amount) { return _collect(); } /// @notice Add ETH straight to the pot. Plain ETH sent to the contract does the same. function donate() external payable { pot += msg.value; emit Donated(msg.sender, msg.value); } // --------------------------------------------------------------------- // Claiming // --------------------------------------------------------------------- /// @notice Withdraw prizes that could not be sent to you (a wallet that refused ETH), or the team's fee share /// when its wallet refused it. Prizes are normally sent straight to the winner. function claim() external nonReentrant returns (uint256 amount) { amount = prizeOf[msg.sender]; prizeOf[msg.sender] = 0; if (amount == 0) revert NothingToClaim(); owed -= amount; _send(msg.sender, amount); emit Claimed(msg.sender, amount); } /// @notice Take back the tokens of your listed locks once their time is up. function unlock(uint256[] calldata lockIds) external nonReentrant returns (uint256 amount) { for (uint256 i = 0; i < lockIds.length; ++i) { uint256 id = lockIds[i]; Lock storage l = locks[id]; if (l.owner != msg.sender) revert NotYourLock(id); if (l.withdrawn) revert AlreadyWithdrawn(id); if (block.timestamp < l.unlockAt) revert StillLocked(id, l.unlockAt); l.withdrawn = true; amount += l.amount; } totalLocked -= amount; IERC20(token).safeTransfer(msg.sender, amount); emit Unlocked(msg.sender, amount); } // --------------------------------------------------------------------- // Team switches // --------------------------------------------------------------------- /// @notice Connect a Grand game if none was set at deployment. Works once and can never be changed. /// @notice Point a game deployed ahead of its launch at the token, once. The token must be a Pons ETH-pair token /// launched by this sign's wallet with this game as its creator fee recipient, so its fees already come here. function setToken(address token_) external onlyTeam nonReentrant { if (token != address(0)) revert TokenAlreadySet(); LaunchedToken memory launch = factory.getLaunchedToken(token_); if (!launch.exists) revert UnknownToken(); if (launch.pairToken != address(0)) revert NotEthPair(); if (launch.deployer != team || launch.creatorFeeRecipient != address(this)) revert NotOurToken(); token = token_; curve = launch.curve; emit TokenSet(token_, launch.curve); } function connectGrand(address grand_) external onlyTeam nonReentrant { if (grand != address(0)) revert GrandAlreadyConnected(); if (grand_ == address(0)) revert ZeroAddress(); grand = grand_; emit GrandConnected(grand_); _forwardGrand(); } /// @notice Start the notice period for moving this token's future Pons fees elsewhere. /// Pots, prizes and locked tokens stay here; only fees earned after the move go to `next`. function proposeFeeRecipient(address next) external onlyTeam withToken { if (next == address(0)) revert ZeroAddress(); pendingFeeRecipient = next; feeRecipientReadyAt = block.timestamp + FEE_ROLE_DELAY; emit FeeRecipientProposed(next, feeRecipientReadyAt); } function cancelFeeRecipient() external onlyTeam { if (pendingFeeRecipient == address(0)) revert NoPendingFeeRecipient(); pendingFeeRecipient = address(0); feeRecipientReadyAt = 0; emit FeeRecipientCancelled(); } /// @notice Carry out a proposed move once its notice has passed. Anyone may call it. function executeFeeRecipient() external nonReentrant { address next = pendingFeeRecipient; if (next == address(0)) revert NoPendingFeeRecipient(); if (block.timestamp < feeRecipientReadyAt) revert FeeRecipientNotReady(feeRecipientReadyAt); _collect(); pendingFeeRecipient = address(0); feeRecipientReadyAt = 0; factory.transferCreatorFeeRecipient(token, next); emit FeeRecipientMoved(next); } // --------------------------------------------------------------------- // Views // --------------------------------------------------------------------- /// @notice The smallest lock that enters right now, in ETH of value. function minLockNow() public view returns (uint256) { uint256 potMin = (pot * minLockPotBps) / BPS; return potMin > minLock ? potMin : minLock; } /// @notice What `tokens` of this sign are worth in ETH right now: what selling them would bring after trading fees /// and price impact, on the bonding curve while it trades and in the Uniswap pool after graduation. function valueOf(uint256 tokens) public view withToken returns (uint256) { if (tokens == 0) return 0; LaunchedToken memory launch = factory.getLaunchedToken(token); if (launch.phase == GraduationPhase.NotGraduated) { (uint256 quoteReserve, uint256 tokenReserve) = IPonsCurve(curve).getReserves(); uint256 gross = (tokens * quoteReserve) / (tokenReserve + tokens); uint256 feeBps = IPonsCurve(curve).feeBps() + IPonsCurve(curve).creatorTaxBps(); return gross - (gross * feeBps) / BPS; } if (launch.phase == GraduationPhase.PoolCreated) { // The same sale as on the curve, against the pool's active liquidity as constant-product reserves, so a // big lock is valued with its price impact rather than at the spot price. ETH is currency0. PoolId id = _poolKey(launch).toId(); (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(id); uint128 liquidity = poolManager.getLiquidity(id); uint256 ethReserve = FullMath.mulDiv(liquidity, Q96, sqrtPriceX96); uint256 tokenReserve = FullMath.mulDiv(liquidity, sqrtPriceX96, Q96); uint256 gross = (tokens * ethReserve) / (tokenReserve + tokens); uint256 feeBps = IPonsHook(hook).hookFeeBps() + launch.creatorTaxBps; return gross - (gross * feeBps) / BPS; } revert NotTrading(); } /// @notice Seconds left on the clock: zero when it has run out or has not started. function timeLeft() external view returns (uint256) { return deadline > block.timestamp ? deadline - block.timestamp : 0; } function lockCount() external view returns (uint256) { return locks.length; } /// @notice Fees Pons has already credited to this game but not yet paid over. function escrowBalance() external view returns (uint256) { return feeEscrow.balanceOf(address(this)); } // --------------------------------------------------------------------- // Internals // --------------------------------------------------------------------- /// @dev Before a lock: pay out a round whose clock has run out (with every fee owed in it), then return the entry /// minimum for the pot as the holder saw it. Fees still waiting at Pons are pulled in after the lock is recorded, /// so they cannot raise the minimum (here or in the Grand game) under a lock already on its way. /// @dev Ends a finished round first, if there is one, and returns the minimum this lock must meet. That is the /// minimum the visitor saw before sending: ending the round pulls in waiting fees and halves the pot, which can /// leave the next pot bigger than the one shown, so the lower of the two applies. function _openRound() private returns (uint256 minimum) { minimum = minLockNow(); if (deadline != 0 && block.timestamp >= deadline) { _collect(); _pay(_settle()); uint256 next = minLockNow(); if (next < minimum) minimum = next; } } function _enter(address locker, uint256 value, uint256 tokens, bool bought) private returns (uint256 lockId) { lockId = locks.length; locks.push(Lock({owner: locker, unlockAt: uint64(block.timestamp + lockLength), withdrawn: false, amount: tokens})); totalLocked += tokens; uint256 id = round; lastLocker = locker; lastLockAt = block.timestamp; if (deadline != 0 || pot >= startFloor) deadline = block.timestamp + clockLength; emit Locked(id, locker, value, tokens, lockId, deadline, bought); // The Grand game decides for itself whether this lock counts there. Nothing it does can undo this lock: // its gas is capped, its reverts are caught, and an address without code is never called. if (grand.code.length > 0) { try IGrandGame(grand).recordLock{gas: GRAND_GAS}(locker, value) {} catch {} } } /// @dev Reports a Buy & lock to the referral list, which credits the buyer's referrers 20% of the team's share /// of `fee` (what this buy pays the game); the game owes that from the team's share until it is paid over. function _reward(address buyer, address referrer, uint256 fee) private { if (referrals.code.length == 0) return; uint256 reward = (((fee * (BPS - POT_BPS - GRAND_BPS)) / BPS) * REFERRAL_BPS) / BPS; try IReferrals(referrals).recordBuy{gas: REFERRAL_GAS}(buyer, referrer, reward) returns ( uint256 credited ) { // Never more than was asked for, so the list can't run up the team's bill. referralDebt += credited < reward ? credited : reward; } catch {} } function _collect() private returns (uint256 amount) { LaunchedToken memory launch = factory.getLaunchedToken(token); // Ask Pons to pay out pending fees. These calls can fail without harm: on the curve when nothing is owed, // in the pool whenever fees wait in the token (then only Pons's own operator may sweep, which it does on a // schedule). Either way the credit lands in the fee escrow, which is read below. if (launch.phase == GraduationPhase.NotGraduated) { try IPonsCurve(curve).sweepFees(0) {} catch {} } else if (launch.phase == GraduationPhase.PoolCreated) { try IPonsHook(hook).sweepPoolFees(PoolId.unwrap(_poolKey(launch).toId()), 0, 0) {} catch {} } if (feeEscrow.balanceOf(address(this)) == 0) return 0; amount = feeEscrow.claim(); uint256 toPot = (amount * POT_BPS) / BPS; uint256 toGrand = (amount * GRAND_BPS) / BPS; uint256 toTeam = amount - toPot - toGrand; pot += toPot; grandSavings += toGrand; // Referral rewards come out of the team's share, as far as this payment covers them. uint256 toReferrals = referralDebt < toTeam ? referralDebt : toTeam; if (toReferrals != 0) { try IReferrals(referrals).fund{value: toReferrals}() { referralDebt -= toReferrals; toTeam -= toReferrals; emit ReferralsFunded(toReferrals); } catch {} } emit FeesCollected(amount, toPot, toGrand, toTeam); _forwardGrand(); (bool sent,) = team.call{value: toTeam}(""); if (!sent) { // Never let the team wallet block the game; it can claim this like a prize. prizeOf[team] += toTeam; owed += toTeam; } } /// @dev Ends the round and credits the winner; returns who won. function _settle() private returns (address winner) { uint256 id = round; uint256 p = pot; uint256 prize = (p * WINNER_BPS) / BPS; winner = lastLocker; rounds[id] = Round({winner: winner, winnerPrize: prize}); prizeOf[winner] += prize; owed += prize; pot = p - prize; deadline = 0; round = id + 1; emit RoundSettled(id, winner, prize, pot); } function _forwardGrand() private { uint256 amount = grandSavings; if (grand == address(0) || amount == 0) return; grandSavings = 0; (bool sent,) = grand.call{value: amount}(""); if (sent) { emit GrandFunded(amount); } else { grandSavings = amount; } } /// @dev Buys with up to `amount` ETH. Returns the ETH spent, the tokens bought, and the part of the trading fees /// on this buy that comes to this game: the creator tax plus the base fee less Pons's own share. function _buy(uint256 amount, uint256 minTokensOut) private returns (uint256 spent, uint256 tokensOut, uint256 fee) { LaunchedToken memory launch = factory.getLaunchedToken(token); uint256 tokensBefore = IERC20(token).balanceOf(address(this)); if (launch.phase == GraduationPhase.NotGraduated) { // The curve refunds any ETH it could not fill to this contract. uint256 ethBefore = address(this).balance - amount; IPonsCurve(curve).buy{value: amount}(amount, minTokensOut, address(this)); spent = amount - (address(this).balance - ethBefore); IPonsCurve c = IPonsCurve(curve); fee = _feeOf(spent, c.feeBps(), c.protocolFeeShareBps(), c.creatorTaxBps()); } else if (launch.phase == GraduationPhase.PoolCreated) { spent = abi.decode(poolManager.unlock(abi.encode(_poolKey(launch), amount, minTokensOut)), (uint256)); IPonsHook h = IPonsHook(hook); fee = _feeOf(spent, h.hookFeeBps(), h.protocolFeeShareBps(), launch.creatorTaxBps); } else { // Between the curve closing and the pool opening nothing can be bought. revert NotTrading(); } tokensOut = IERC20(token).balanceOf(address(this)) - tokensBefore; } /// @dev Exact-input ETH -> token swap in the graduated pool. Only reachable through `_buy`. function unlockCallback(bytes calldata data) external returns (bytes memory) { if (msg.sender != address(poolManager)) revert NotPoolManager(); (PoolKey memory key, uint256 amountIn, uint256 minTokensOut) = abi.decode(data, (PoolKey, uint256, uint256)); BalanceDelta delta = poolManager.swap( key, SwapParams({ zeroForOne: true, amountSpecified: -int256(amountIn), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 }), "" ); uint256 paid = uint256(uint128(-delta.amount0())); uint256 got = uint256(uint128(delta.amount1())); if (got < minTokensOut) revert TooLittleReceived(got, minTokensOut); poolManager.settle{value: paid}(); poolManager.take(key.currency1, address(this), got); return abi.encode(paid); } /// @dev What a trade of `amount` pays this game: the creator tax, plus the base fee less Pons's share of it. function _feeOf(uint256 amount, uint256 feeBps, uint256 protocolShareBps, uint256 taxBps) private pure returns (uint256) { uint256 baseFee = (amount * feeBps) / BPS; return baseFee - (baseFee * protocolShareBps) / BPS + (amount * taxBps) / BPS; } /// @dev Native ETH sorts below every token address, so it is always currency0. function _poolKey(LaunchedToken memory launch) private view returns (PoolKey memory) { return PoolKey({ currency0: Currency.wrap(address(0)), currency1: Currency.wrap(token), fee: launch.poolFee, tickSpacing: launch.tickSpacing, hooks: IHooks(hook) }); } /// @dev Sends `to` their credited prizes with capped gas. On a refusal the credit stays for them to claim. function _pay(address to) private returns (uint256 amount) { amount = prizeOf[to]; if (amount == 0) return 0; prizeOf[to] = 0; owed -= amount; (bool sent,) = to.call{value: amount, gas: PAYOUT_GAS}(""); if (!sent) { prizeOf[to] = amount; owed += amount; return 0; } emit PaidOut(to, amount); } function _send(address to, uint256 amount) private { (bool sent,) = to.call{value: amount}(""); if (!sent) revert SendFailed(); } /// @dev Pons fee payouts, curve refunds and pool settlement arrive here and are counted where /// they are triggered. Anything else sent to the contract is treated as a donation to the pot. receive() external payable { if (msg.sender == address(feeEscrow) || msg.sender == curve || msg.sender == address(poolManager)) return; pot += msg.value; emit Donated(msg.sender, msg.value); } }