// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; import {GMContractCustody, IPonsV2FeeEscrowClaim, IERC20BalanceTransfer, IGMDrawVerifier} from "./GMContractCustody.sol"; interface IPonsV2BondingCurveV5 { function token() external view returns (address); function pairToken() external view returns (address); function buybackEnabled() external view returns (bool); function sweepFees(uint256 minBuybackTokensOut) external; } interface IPonsV2LaunchFactoryV5 { enum GraduationPhase { NotGraduated, Swept, PoolCreated, Rescued } struct LaunchedToken { address token; address curve; address deployer; address creatorFeeRecipient; address pairToken; uint256 graduationThreshold; uint24 poolFee; int24 tickSpacing; uint16 creatorTaxBps; bool buybackEnabled; GraduationPhase phase; uint256 sweptQuote; uint256 sweptTokens; uint256 sweptAt; bool exists; } function feeEscrow() external view returns (address); function getLaunchedToken(address token) external view returns (LaunchedToken memory); function transferCreatorFeeRecipient(address token, address newRecipient) external; } /** * Custody v5 adds two narrow Pons integrations to the unchanged v4 lifecycle: * a permissionless, destination-free curve-fee sweep and a recovery-Safe-only * creator-recipient handoff. Neither path is coupled to claim, commit, or settle. */ contract GMContractCustodyV5 is GMContractCustody { IPonsV2BondingCurveV5 public immutable PONS_CURVE; IPonsV2LaunchFactoryV5 public immutable PONS_FACTORY; error ContractCodeRequired(address target); error CurveTokenMismatch(address actual); error CurvePairTokenMismatch(address actual); error FactoryEscrowMismatch(address actual); error LaunchBindingMismatch(); error CustodyNotCreatorFeeRecipient(address actual); error BuybackEnabled(); error BuybackStateMismatch(); error InvalidRecoveryRecipient(); error RecoveryRecipientMismatch(address actual); event CurveFeesSwept(address indexed executor, address indexed curve); event CreatorFeeRecipientTransferred(address indexed recoverySafe, address indexed newRecipient); constructor( IPonsV2FeeEscrowClaim ponsEscrow, address gmToken, IERC20BalanceTransfer usdg, address reserveSafe, address recoverySafe, address committer, IGMDrawVerifier drawVerifier, uint256 minimumClaimableUsdg, IPonsV2BondingCurveV5 ponsCurve, IPonsV2LaunchFactoryV5 ponsFactory ) GMContractCustody( ponsEscrow, gmToken, usdg, reserveSafe, recoverySafe, committer, drawVerifier, minimumClaimableUsdg ) { if (address(ponsCurve) == address(0) || address(ponsFactory) == address(0)) revert ZeroAddress(); if (address(ponsCurve).code.length == 0) revert ContractCodeRequired(address(ponsCurve)); if (address(ponsFactory).code.length == 0) revert ContractCodeRequired(address(ponsFactory)); address curveToken = ponsCurve.token(); if (curveToken != gmToken) revert CurveTokenMismatch(curveToken); address curvePairToken = ponsCurve.pairToken(); if (curvePairToken != address(usdg)) revert CurvePairTokenMismatch(curvePairToken); address factoryEscrow = ponsFactory.feeEscrow(); if (factoryEscrow != address(ponsEscrow)) revert FactoryEscrowMismatch(factoryEscrow); IPonsV2LaunchFactoryV5.LaunchedToken memory launch = ponsFactory.getLaunchedToken(gmToken); if (!launch.exists || launch.token != gmToken || launch.curve != address(ponsCurve) || launch.pairToken != address(usdg)) revert LaunchBindingMismatch(); PONS_CURVE = ponsCurve; PONS_FACTORY = ponsFactory; } /** * Permissionless liveness trigger with no caller-selected target, amount, or * destination. A zero buyback bound is safe only while the factory's * authoritative launch record says buyback is disabled, so this fails closed * if that policy changes. Reverts leave custody draw accounting untouched. */ function sweepCurveFees() external nonReentrant whenRunning { IPonsV2LaunchFactoryV5.LaunchedToken memory launch = PONS_FACTORY.getLaunchedToken(GM_TOKEN); if (!launch.exists || launch.curve != address(PONS_CURVE)) revert LaunchBindingMismatch(); if (launch.creatorFeeRecipient != address(this)) { revert CustodyNotCreatorFeeRecipient(launch.creatorFeeRecipient); } bool curveBuybackEnabled = PONS_CURVE.buybackEnabled(); if (launch.buybackEnabled != curveBuybackEnabled) revert BuybackStateMismatch(); if (launch.buybackEnabled) revert BuybackEnabled(); PONS_CURVE.sweepFees(0); emit CurveFeesSwept(msg.sender, address(PONS_CURVE)); } /** Irreversible, separately invoked recovery action; not a generic proxy. */ function transferCreatorFeeRecipient(address newRecipient) external onlyRecoverySafe nonReentrant { if (newRecipient == address(0) || newRecipient == address(this)) revert InvalidRecoveryRecipient(); PONS_FACTORY.transferCreatorFeeRecipient(GM_TOKEN, newRecipient); address actualRecipient = PONS_FACTORY.getLaunchedToken(GM_TOKEN).creatorFeeRecipient; if (actualRecipient != newRecipient) revert RecoveryRecipientMismatch(actualRecipient); emit CreatorFeeRecipientTransferred(msg.sender, newRecipient); } }