r/source code
Every custom contract.
Every executable line.
These are the complete custom Solidity contracts behind the fee split, liquidity injection, reference oracle, and graduation lock. Imported OpenZeppelin, Uniswap, and protocol interfaces are external dependencies.Claims project fees, records the fixed split, and exposes the public injection entry point.
LiquidityFeeReceiver.solSolidity · 0.8.26
// SPDX-License-Identifier: MIT// Website: https://reddit-injection.fund/pragma solidity ^0.8.26;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 "@uniswap/v4-core/src/interfaces/IPoolManager.sol";import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";import {IPonsFeeEscrow, IPonsFactory, IPonsCurve, IPonsHook} from "./interfaces/IPons.sol";import {IReferenceOracle} from "./interfaces/IReferenceOracle.sol";import {LockedLiquidityVault} from "./LockedLiquidityVault.sol";/// @notice Pons fee recipient with fixed 50/50 accounting and public execution./// @dev Bootstrapper can bind one verified launch only; no further admin powers.contract LiquidityFeeReceiver is ReentrancyGuard { using SafeERC20 for IERC20; struct Config { IPonsFeeEscrow escrow; IPonsFactory factory; IPoolManager manager; IReferenceOracle oracle; address hook; IERC20 quote; address treasury; address bootstrapper; uint256 minBatch; uint256 maxBatch; } IPonsFeeEscrow public immutable escrow; IPonsFactory public immutable factory; IPoolManager public immutable manager; IReferenceOracle public immutable oracle; address public immutable hook; IERC20 public immutable quote; address public immutable treasury; address public immutable bootstrapper; uint256 public immutable minBatch; uint256 public immutable maxBatch; LockedLiquidityVault.Limits private executionLimits; address public token; address public curve; LockedLiquidityVault public vault; uint256 public totalClaimed; uint256 public liquidityBudget; uint256 public treasuryAccrued; uint256 public totalTreasuryPaid; uint256 public totalSentToVault; error InvalidConfiguration(); error Unauthorized(); error InvalidLaunch(); error InexactTransfer(); error InsufficientBudget(); event LaunchBound(address indexed token, address indexed vault, bytes32 poolId); event FeesClaimed(uint256 received, uint256 liquidityAllocation, uint256 treasuryAllocation); event LiquidityDonated(uint256 amount); event InjectionCompleted(address indexed caller, uint256 budgetSent, uint128 liquidityAdded); event InjectionDeferred(bytes reason); event SweepDeferred(bytes reason); event TreasuryPaid(uint256 amount); constructor(Config memory config, LockedLiquidityVault.Limits memory limits) { if (address(config.escrow).code.length == 0 || address(config.factory).code.length == 0 || address(config.manager).code.length == 0 || address(config.oracle).code.length == 0 || config.hook.code.length == 0 || address(config.quote).code.length == 0 || config.treasury == address(0) || config.treasury == address(this) || config.bootstrapper == address(0) || config.minBatch == 0 || config.maxBatch < config.minBatch) { revert InvalidConfiguration(); } if (config.factory.poolManager() != address(config.manager) || config.factory.memeHook() != config.hook || config.factory.feeEscrow() != address(config.escrow) || uint160(config.hook) & 0x3fff != 0x2044 || limits.maxOracleAge == 0 || limits.maxSwapQuote == 0 || limits.maxSwapQuote > uint256(uint128(type(int128).max)) || limits.maxTickDeviation <= 0 || limits.maxTickDeviation > 200 || limits.maxSwapLossBps == 0 || limits.maxSwapLossBps > 500) revert InvalidConfiguration(); escrow = config.escrow; factory = config.factory; manager = config.manager; oracle = config.oracle; hook = config.hook; quote = config.quote; treasury = config.treasury; bootstrapper = config.bootstrapper; minBatch = config.minBatch; maxBatch = config.maxBatch; executionLimits = limits; } function bindLaunch(address token_) external nonReentrant { if (msg.sender != bootstrapper) revert Unauthorized(); if (token != address(0) || token_ == address(quote) || token_.code.length == 0) revert InvalidLaunch(); IPonsFactory.Launch memory launch = factory.getLaunchedToken(token_); if (!launch.exists || launch.token != token_ || launch.creatorFeeRecipient != address(this) || launch.pairToken != address(quote) || launch.buybackEnabled || launch.creatorTaxBps != 100 || launch.poolFee != 0 || launch.tickSpacing <= 0 || launch.curve.code.length == 0 || launch.phase == 3) { revert InvalidLaunch(); } IPonsFactory.FeePolicy memory policy = factory.getLaunchFeePolicy(token_); if (policy.hookFeeBps != 100 || policy.protocolFeeShareBps != 3000 || IPonsCurve(launch.curve).feeBps() != 100) revert InvalidLaunch(); token = token_; curve = launch.curve; bool quoteFirst = address(quote) < token_; PoolKey memory key = PoolKey({ currency0: Currency.wrap(quoteFirst ? address(quote) : token_), currency1: Currency.wrap(quoteFirst ? token_ : address(quote)), fee: launch.poolFee, tickSpacing: launch.tickSpacing, hooks: IHooks(hook) }); vault = new LockedLiquidityVault(address(this), manager, oracle, key, quote, executionLimits); emit LaunchBound(token_, address(vault), vault.poolId()); } /// @notice Collect already-claimable escrow funds even before binding/graduation. function claimFees() external nonReentrant returns (uint256) { return _claim(); } /// @notice Attempts an authorized Pons sweep, claims, then tries one bounded batch. /// Unsafe/unavailable injections preserve the newly claimed budget for later. function claimFeesAndInject() external nonReentrant returns (uint256 claimed, uint128 added) { if (token != address(0)) _trySweep(); claimed = _claim(); if (token == address(0) || liquidityBudget < minBatch) return (claimed, 0); IPonsFactory.Launch memory launch = factory.getLaunchedToken(token); if (launch.phase != 2) return (claimed, 0); uint256 batch = liquidityBudget < maxBatch ? liquidityBudget : maxBatch; try this.executeInjection(batch) returns (uint128 result) { added = result; emit InjectionCompleted(msg.sender, batch, result); } catch (bytes memory reason) { emit InjectionDeferred(reason); } } /// @dev External self-call provides atomic rollback of a failed injection only. function executeInjection(uint256 batch) external returns (uint128 added) { if (msg.sender != address(this)) revert Unauthorized(); if (batch < minBatch || batch > maxBatch || batch > liquidityBudget) revert InsufficientBudget(); liquidityBudget -= batch; quote.forceApprove(address(vault), batch); added = vault.inject(batch); quote.forceApprove(address(vault), 0); totalSentToVault += batch; } /// @notice Anyone can deliver the accrued treasury share, only to its fixed address. function payTreasury() external nonReentrant returns (uint256 amount) { amount = treasuryAccrued; treasuryAccrued = 0; totalTreasuryPaid += amount; if (amount != 0) quote.safeTransfer(treasury, amount); emit TreasuryPaid(amount); } /// @notice Direct quote donations are 100% liquidity funding, never split again. function syncDonations() external nonReentrant returns (uint256 amount) { amount = quote.balanceOf(address(this)) - liquidityBudget - treasuryAccrued; liquidityBudget += amount; emit LiquidityDonated(amount); } function _claim() private returns (uint256 received) { if (escrow.balanceOfToken(address(this), address(quote)) == 0) return 0; uint256 beforeBalance = quote.balanceOf(address(this)); uint256 reported = escrow.claimToken(address(quote)); received = quote.balanceOf(address(this)) - beforeBalance; if (reported != received) revert InexactTransfer(); // Cumulative rounding prevents repeated one-unit claims from changing the split. uint256 treasuryShare = (totalClaimed + received) / 2 - totalClaimed / 2; totalClaimed += received; treasuryAccrued += treasuryShare; liquidityBudget += received - treasuryShare; emit FeesClaimed(received, received - treasuryShare, treasuryShare); } function _trySweep() private { IPonsFactory.Launch memory launch = factory.getLaunchedToken(token); // A Pons recipient redirection does not move our previously accrued budget. if (launch.creatorFeeRecipient != address(this)) return; if (launch.phase == 0) { try IPonsCurve(curve).sweepFees(0) {} catch (bytes memory reason) { emit SweepDeferred(reason); } } else if (launch.phase == 2) { try IPonsHook(hook).sweepPoolFees(vault.poolId(), 0, 0) {} catch (bytes memory reason) { emit SweepDeferred(reason); } } }}Balances the stock-paired assets and increases one permanent Uniswap v4 liquidity position.
LockedLiquidityVault.solSolidity · 0.8.26
// SPDX-License-Identifier: MIT// Website: https://reddit-injection.fund/pragma solidity ^0.8.26;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 "@uniswap/v4-core/src/interfaces/IPoolManager.sol";import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";import {FullMath} from "@uniswap/v4-core/src/libraries/FullMath.sol";import {IReferenceOracle} from "./interfaces/IReferenceOracle.sol";/// @notice Owns a direct v4 core position. There is no NFT, owner, upgrade, rescue,/// approval, arbitrary call, or negative-liquidity path. Residual assets stay here.contract LockedLiquidityVault is IUnlockCallback, ReentrancyGuard { using SafeERC20 for IERC20; using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; struct Limits { uint256 maxOracleAge; uint256 maxSwapQuote; int24 maxTickDeviation; uint16 maxSwapLossBps; } uint256 private constant Q96 = 1 << 96; bytes32 public constant POSITION_SALT = keccak256("redif.permanent.liquidity.v1"); address public immutable receiver; IPoolManager public immutable manager; IReferenceOracle public immutable oracle; IERC20 public immutable quote; IERC20 public immutable projectToken; bool public immutable quoteIs0; int24 public immutable tickLower; int24 public immutable tickUpper; uint256 public immutable maxOracleAge; uint256 public immutable maxSwapQuote; int24 public immutable maxTickDeviation; uint16 public immutable maxSwapLossBps; PoolKey public poolKey; uint128 public totalLiquidityAdded; bool private unlocking; error InvalidConfiguration(); error Unauthorized(); error InvalidOracle(); error UnsafePrice(); error InexactTransfer(); error NoLiquidity(); error BadSwap(); event LiquidityInjected(uint256 quoteReceived, uint128 liquidityAdded, uint256 quoteRemaining, uint256 tokenRemaining); constructor( address receiver_, IPoolManager manager_, IReferenceOracle oracle_, PoolKey memory key_, IERC20 quote_, Limits memory limits ) { address c0 = Currency.unwrap(key_.currency0); address c1 = Currency.unwrap(key_.currency1); if (receiver_ == address(0) || address(manager_).code.length == 0 || address(oracle_).code.length == 0 || c0 == address(0) || c0 >= c1 || (address(quote_) != c0 && address(quote_) != c1) || key_.fee != 0 || key_.tickSpacing <= 0 || key_.tickSpacing > 32767 || limits.maxOracleAge == 0 || limits.maxSwapQuote == 0 || limits.maxSwapQuote > uint256(uint128(type(int128).max)) || limits.maxTickDeviation <= 0 || limits.maxTickDeviation > 200 || limits.maxSwapLossBps == 0 || limits.maxSwapLossBps > 500) revert InvalidConfiguration(); receiver = receiver_; manager = manager_; oracle = oracle_; poolKey = key_; quote = quote_; quoteIs0 = address(quote_) == c0; projectToken = IERC20(address(quote_) == c0 ? c1 : c0); tickLower = TickMath.minUsableTick(key_.tickSpacing); tickUpper = TickMath.maxUsableTick(key_.tickSpacing); maxOracleAge = limits.maxOracleAge; maxSwapQuote = limits.maxSwapQuote; maxTickDeviation = limits.maxTickDeviation; maxSwapLossBps = limits.maxSwapLossBps; } function poolId() public view returns (bytes32) { return PoolId.unwrap(poolKey.toId()); } function inject(uint256 amount) external nonReentrant returns (uint128 added) { if (msg.sender != receiver) revert Unauthorized(); uint256 beforeBalance = quote.balanceOf(address(this)); if (amount != 0) quote.safeTransferFrom(receiver, address(this), amount); if (quote.balanceOf(address(this)) != beforeBalance + amount) revert InexactTransfer(); unlocking = true; added = abi.decode(manager.unlock(""), (uint128)); unlocking = false; totalLiquidityAdded += added; emit LiquidityInjected(amount, added, quote.balanceOf(address(this)), projectToken.balanceOf(address(this))); } function unlockCallback(bytes calldata) external returns (bytes memory) { if (msg.sender != address(manager) || !unlocking) revert Unauthorized(); // Consume the callback authorization before making external token/hook calls. unlocking = false; PoolKey memory key = poolKey; (int24 referenceTick, uint256 timestamp) = oracle.read(poolId()); if (timestamp == 0 || timestamp > block.timestamp || block.timestamp - timestamp > maxOracleAge || referenceTick <= tickLower + maxTickDeviation || referenceTick >= tickUpper - maxTickDeviation) { revert InvalidOracle(); } _checkPrice(key, referenceTick); uint160 referencePrice = TickMath.getSqrtPriceAtTick(referenceTick); uint256 availableQuote = quote.balanceOf(address(this)); uint256 tokenValue = _convert(projectToken.balanceOf(address(this)), referencePrice, !quoteIs0); // Reuse residual tokens before buying more. Any imbalance remains locked. uint256 swapAmount = availableQuote > tokenValue ? (availableQuote - tokenValue) / 2 : 0; if (swapAmount > maxSwapQuote) swapAmount = maxSwapQuote; if (swapAmount != 0) { int24 limitTick = quoteIs0 ? referenceTick - maxTickDeviation : referenceTick + maxTickDeviation; BalanceDelta delta = manager.swap(key, SwapParams({ zeroForOne: quoteIs0, amountSpecified: -int256(swapAmount), sqrtPriceLimitX96: TickMath.getSqrtPriceAtTick(limitTick) }), ""); int128 input = quoteIs0 ? delta.amount0() : delta.amount1(); int128 output = quoteIs0 ? delta.amount1() : delta.amount0(); if (input >= 0 || output <= 0) revert BadSwap(); uint256 spent = uint256(-int256(input)); uint256 expected = _convert(spent, referencePrice, quoteIs0); uint256 minimum = FullMath.mulDiv(expected, 10000 - maxSwapLossBps, 10000); if (spent > swapAmount || minimum == 0 || uint256(uint128(output)) < minimum) revert BadSwap(); _settle(key.currency0, delta.amount0()); _settle(key.currency1, delta.amount1()); } uint160 price = _checkPrice(key, referenceTick); uint128 liquidity = _liquidityForBalances(key, price); if (liquidity == 0) revert NoLiquidity(); (BalanceDelta added,) = manager.modifyLiquidity(key, ModifyLiquidityParams({ tickLower: tickLower, tickUpper: tickUpper, liquidityDelta: int256(uint256(liquidity)), salt: POSITION_SALT }), ""); _settle(key.currency0, added.amount0()); _settle(key.currency1, added.amount1()); _checkPrice(key, referenceTick); return abi.encode(liquidity); } function _checkPrice(PoolKey memory key, int24 referenceTick) private view returns (uint160 price) { (uint160 current, int24 tick,,) = manager.getSlot0(key.toId()); int256 distance = int256(tick) - int256(referenceTick); if (current == 0 || distance >= maxTickDeviation || distance <= -int256(maxTickDeviation)) revert UnsafePrice(); return current; } function _convert(uint256 amount, uint160 sqrtPrice, bool zeroForOne) private pure returns (uint256) { // Same precision split used by Uniswap's oracle quote calculation. if (sqrtPrice <= type(uint128).max) { uint256 ratioX192 = uint256(sqrtPrice) * sqrtPrice; return zeroForOne ? FullMath.mulDiv(amount, ratioX192, 1 << 192) : FullMath.mulDiv(amount, 1 << 192, ratioX192); } uint256 ratioX128 = FullMath.mulDiv(sqrtPrice, sqrtPrice, 1 << 64); return zeroForOne ? FullMath.mulDiv(amount, ratioX128, 1 << 128) : FullMath.mulDiv(amount, 1 << 128, ratioX128); } function _liquidityForBalances(PoolKey memory key, uint160 price) private view returns (uint128) { uint160 lower = TickMath.getSqrtPriceAtTick(tickLower); uint160 upper = TickMath.getSqrtPriceAtTick(tickUpper); uint256 amount0 = IERC20(Currency.unwrap(key.currency0)).balanceOf(address(this)); uint256 amount1 = IERC20(Currency.unwrap(key.currency1)).balanceOf(address(this)); uint256 l0 = FullMath.mulDiv(amount0, FullMath.mulDiv(price, upper, Q96), upper - price); uint256 l1 = FullMath.mulDiv(amount1, Q96, price - lower); uint256 result = l0 < l1 ? l0 : l1; if (result > uint256(uint128(type(int128).max))) revert NoLiquidity(); return uint128(result); } function _settle(Currency currency, int128 delta) private { if (delta < 0) { uint256 amount = uint256(-int256(delta)); manager.sync(currency); IERC20(Currency.unwrap(currency)).safeTransfer(address(manager), amount); if (manager.settle() != amount) revert InexactTransfer(); } else if (delta > 0) { manager.take(currency, address(this), uint256(uint128(delta))); } }}Accepts fresh reference ticks only when the configured signer quorum approves the same report.
SignedReferenceOracle.solSolidity · 0.8.26
// SPDX-License-Identifier: MIT// Website: https://reddit-injection.fund/pragma solidity ^0.8.26;import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";import {IReferenceOracle} from "./interfaces/IReferenceOracle.sol";/// @notice Immutable-quorum reference tick oracle for the liquidity vault./// @dev Anyone may relay a report, but every accepted report must carry enough/// signatures from the signer set fixed at construction. Signatures must be/// ordered by recovered signer address so duplicates are impossible.contract SignedReferenceOracle is IReferenceOracle, EIP712 { struct StoredReport { int24 tick; uint64 observedAt; uint64 sequence; } bytes32 public constant REPORT_TYPEHASH = keccak256( "TickReport(bytes32 poolId,int24 tick,uint64 observedAt,uint64 validUntil,uint64 sequence)" ); int24 private constant MIN_TICK = -887272; int24 private constant MAX_TICK = 887272; uint8 public immutable threshold; uint64 public immutable maxReportValidity; mapping(address => bool) public isSigner; mapping(bytes32 => StoredReport) private reports; error InvalidConfiguration(); error InvalidReport(); error InsufficientSignatures(); error SignaturesNotStrictlyOrdered(); event ReportAccepted( bytes32 indexed poolId, int24 tick, uint64 observedAt, uint64 validUntil, uint64 sequence ); constructor(address[] memory signers, uint8 threshold_, uint64 maxReportValidity_) EIP712("REDIF Signed Reference Oracle", "1") { if ( signers.length == 0 || signers.length > type(uint8).max || threshold_ == 0 || threshold_ > signers.length || maxReportValidity_ == 0 ) revert InvalidConfiguration(); address previous; for (uint256 i; i < signers.length; ++i) { address signer = signers[i]; // A sorted constructor list makes duplicates obvious and produces a // deterministic deployment configuration. if (signer == address(0) || signer <= previous) revert InvalidConfiguration(); isSigner[signer] = true; previous = signer; } threshold = threshold_; maxReportValidity = maxReportValidity_; } /// @notice Returns the latest accepted report for a pool. function read(bytes32 poolId) external view returns (int24 tick, uint256 updatedAt) { StoredReport memory report = reports[poolId]; return (report.tick, report.observedAt); } function latestSequence(bytes32 poolId) external view returns (uint64) { return reports[poolId].sequence; } function hashReport( bytes32 poolId, int24 tick, uint64 observedAt, uint64 validUntil, uint64 sequence ) public view returns (bytes32) { return _hashTypedDataV4( keccak256(abi.encode(REPORT_TYPEHASH, poolId, tick, observedAt, validUntil, sequence)) ); } /// @notice Relays a quorum-approved tick report. The relayer needs no role. function submit( bytes32 poolId, int24 tick, uint64 observedAt, uint64 validUntil, uint64 sequence, bytes[] calldata signatures ) external { StoredReport memory current = reports[poolId]; if ( poolId == bytes32(0) || tick < MIN_TICK || tick > MAX_TICK || observedAt == 0 || observedAt > block.timestamp || validUntil < block.timestamp || validUntil < observedAt || validUntil - observedAt > maxReportValidity || sequence <= current.sequence || observedAt <= current.observedAt ) revert InvalidReport(); if (signatures.length < threshold) revert InsufficientSignatures(); bytes32 digest = hashReport(poolId, tick, observedAt, validUntil, sequence); address previous; for (uint256 i; i < signatures.length; ++i) { address signer = ECDSA.recover(digest, signatures[i]); if (!isSigner[signer]) revert InsufficientSignatures(); if (signer <= previous) revert SignaturesNotStrictlyOrdered(); previous = signer; } reports[poolId] = StoredReport(tick, observedAt, sequence); emit ReportAccepted(poolId, tick, observedAt, validUntil, sequence); }}Locks one owner-funded REDIF deposit for 30 days or changes it to an irreversible eternal lock.
RedifGraduationLocker.solSolidity · 0.8.26
// SPDX-License-Identifier: MIT// Website: https://reddit-injection.fund/pragma solidity ^0.8.26;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";interface IRedifLaunchReceiver { function token() external view returns (address); function vault() external view returns (address);}/// @title REDIF Graduation Locker/// @notice Binds once to the REDIF token verified by the immutable launch receiver,/// then holds one owner-funded deposit for 30 days. Before the deadline, the owner/// may irreversibly make the lock eternal. Otherwise, only that same owner may/// withdraw the entire REDIF balance once the deadline passes./// @dev There is deliberately no ownership transfer, rescue, upgrade, pause,/// extension, early-withdrawal, arbitrary token selection, or third-party/// state-changing function.contract RedifGraduationLocker is ReentrancyGuard { using SafeERC20 for IERC20; uint64 public constant LOCK_DURATION = 30 days; enum State { AwaitingBinding, AwaitingDeposit, Locked, EternallyLocked, Withdrawn } IRedifLaunchReceiver public immutable receiver; address public immutable owner; IERC20 public token; State public state; uint64 public unlockAt; uint256 public initialLockedAmount; error Unauthorized(); error InvalidConfiguration(); error InvalidAmount(); error InvalidState(); error InvalidBinding(); error LockExpired(); error LockActive(); error InexactTransfer(); event TokenBound(address indexed token); event LockStarted(uint256 amount, uint64 unlockAt); event EternallyLocked(uint256 balance); event Withdrawn(uint256 amount); modifier onlyOwner() { if (msg.sender != owner) revert Unauthorized(); _; } constructor(IRedifLaunchReceiver receiver_, address owner_) { if (address(receiver_).code.length == 0 || owner_ == address(0) || owner_ == address(this)) { revert InvalidConfiguration(); } receiver = receiver_; owner = owner_; } /// @notice Permanently records the project token already verified by the /// immutable receiver's one-time Pons launch binding. /// @dev No token address is supplied by the caller. A created vault proves /// bindLaunch() completed for the receiver's exact token and pool. function bindToken() external onlyOwner { if (state != State.AwaitingBinding) revert InvalidState(); address boundToken = receiver.token(); if (boundToken.code.length == 0 || receiver.vault().code.length == 0) revert InvalidBinding(); token = IERC20(boundToken); state = State.AwaitingDeposit; emit TokenBound(boundToken); } /// @notice Pulls the single locked deposit from the owner and starts 30 days. /// @dev The owner must approve this contract first. Fee-on-transfer behavior /// is rejected so the recorded amount always equals the amount actually held. function startLock(uint256 amount) external onlyOwner nonReentrant { if (state != State.AwaitingDeposit) revert InvalidState(); if (amount == 0) revert InvalidAmount(); uint256 balanceBefore = token.balanceOf(address(this)); token.safeTransferFrom(owner, address(this), amount); if (token.balanceOf(address(this)) - balanceBefore != amount) revert InexactTransfer(); uint256 deadline = block.timestamp + LOCK_DURATION; if (deadline > type(uint64).max) revert InvalidConfiguration(); initialLockedAmount = amount; unlockAt = uint64(deadline); state = State.Locked; emit LockStarted(amount, uint64(deadline)); } /// @notice Irreversibly disables withdrawal while the 30-day lock is active. function eternallyLock() external onlyOwner { if (state != State.Locked) revert InvalidState(); if (block.timestamp >= unlockAt) revert LockExpired(); state = State.EternallyLocked; emit EternallyLocked(token.balanceOf(address(this))); } /// @notice Withdraws all REDIF held by the locker after the 30-day deadline. function withdraw() external onlyOwner nonReentrant returns (uint256 amount) { if (state != State.Locked) revert InvalidState(); if (block.timestamp < unlockAt) revert LockActive(); state = State.Withdrawn; amount = token.balanceOf(address(this)); token.safeTransfer(owner, amount); emit Withdrawn(amount); }}