PondManager

Description:

Multi-signature wallet contract requiring multiple confirmations for transaction execution.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.16 ^0.8.0 ^0.8.19 ^0.8.20 ^0.8.4;

// lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol

// solhint-disable-next-line interface-starts-with-i
interface AggregatorV3Interface {
  function decimals() external view returns (uint8);

  function description() external view returns (string memory);

  function version() external view returns (uint256);

  function getRoundData(
    uint80 _roundId
  ) external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

  function latestRoundData()
    external
    view
    returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}

// lib/openzeppelin-contracts/contracts/utils/Context.sol

// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol)

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }

    function _contextSuffixLength() internal view virtual returns (uint256) {
        return 0;
    }
}

// lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol

// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol)

/**
 * @dev Interface of the ERC-20 standard as defined in the ERC.
 */
interface IERC20 {
    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);

    /**
     * @dev Returns the value of tokens in existence.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns the value of tokens owned by `account`.
     */
    function balanceOf(address account) external view returns (uint256);

    /**
     * @dev Moves a `value` amount of tokens from the caller's account to `to`.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transfer(address to, uint256 value) external returns (bool);

    /**
     * @dev Returns the remaining number of tokens that `spender` will be
     * allowed to spend on behalf of `owner` through {transferFrom}. This is
     * zero by default.
     *
     * This value changes when {approve} or {transferFrom} are called.
     */
    function allowance(address owner, address spender) external view returns (uint256);

    /**
     * @dev Sets a `value` amount of tokens as the allowance of `spender` over the
     * caller's tokens.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * IMPORTANT: Beware that changing an allowance with this method brings the risk
     * that someone may use both the old and the new allowance by unfortunate
     * transaction ordering. One possible solution to mitigate this race
     * condition is to first reduce the spender's allowance to 0 and set the
     * desired value afterwards:
     * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 value) external returns (bool);

    /**
     * @dev Moves a `value` amount of tokens from `from` to `to` using the
     * allowance mechanism. `value` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/interfaces/IOwnable.sol

interface IOwnable {
  function owner() external returns (address);

  function transferOwnership(address recipient) external;

  function acceptOwnership() external;
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/interfaces/IVRFMigratableConsumerV2Plus.sol

/// @notice The IVRFMigratableConsumerV2Plus interface defines the
/// @notice method required to be implemented by all V2Plus consumers.
/// @dev This interface is designed to be used in VRFConsumerBaseV2Plus.
interface IVRFMigratableConsumerV2Plus {
  event CoordinatorSet(address vrfCoordinator);

  /// @notice Sets the VRF Coordinator address
  /// @notice This method should only be callable by the coordinator or contract owner
  function setCoordinator(address vrfCoordinator) external;
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/interfaces/IVRFSubscriptionV2Plus.sol

/// @notice The IVRFSubscriptionV2Plus interface defines the subscription
/// @notice related methods implemented by the V2Plus coordinator.
interface IVRFSubscriptionV2Plus {
  /**
   * @notice Add a consumer to a VRF subscription.
   * @param subId - ID of the subscription
   * @param consumer - New consumer which can use the subscription
   */
  function addConsumer(uint256 subId, address consumer) external;

  /**
   * @notice Remove a consumer from a VRF subscription.
   * @param subId - ID of the subscription
   * @param consumer - Consumer to remove from the subscription
   */
  function removeConsumer(uint256 subId, address consumer) external;

  /**
   * @notice Cancel a subscription
   * @param subId - ID of the subscription
   * @param to - Where to send the remaining LINK to
   */
  function cancelSubscription(uint256 subId, address to) external;

  /**
   * @notice Accept subscription owner transfer.
   * @param subId - ID of the subscription
   * @dev will revert if original owner of subId has
   * not requested that msg.sender become the new owner.
   */
  function acceptSubscriptionOwnerTransfer(uint256 subId) external;

  /**
   * @notice Request subscription owner transfer.
   * @param subId - ID of the subscription
   * @param newOwner - proposed new owner of the subscription
   */
  function requestSubscriptionOwnerTransfer(uint256 subId, address newOwner) external;

  /**
   * @notice Create a VRF subscription.
   * @return subId - A unique subscription id.
   * @dev You can manage the consumer set dynamically with addConsumer/removeConsumer.
   * @dev Note to fund the subscription with LINK, use transferAndCall. For example
   * @dev  LINKTOKEN.transferAndCall(
   * @dev    address(COORDINATOR),
   * @dev    amount,
   * @dev    abi.encode(subId));
   * @dev Note to fund the subscription with Native, use fundSubscriptionWithNative. Be sure
   * @dev  to send Native with the call, for example:
   * @dev COORDINATOR.fundSubscriptionWithNative{value: amount}(subId);
   */
  function createSubscription() external returns (uint256 subId);

  /**
   * @notice Get a VRF subscription.
   * @param subId - ID of the subscription
   * @return balance - LINK balance of the subscription in juels.
   * @return nativeBalance - native balance of the subscription in wei.
   * @return reqCount - Requests count of subscription.
   * @return owner - owner of the subscription.
   * @return consumers - list of consumer address which are able to use this subscription.
   */
  function getSubscription(
    uint256 subId
  )
    external
    view
    returns (uint96 balance, uint96 nativeBalance, uint64 reqCount, address owner, address[] memory consumers);

  /*
   * @notice Check to see if there exists a request commitment consumers
   * for all consumers and keyhashes for a given sub.
   * @param subId - ID of the subscription
   * @return true if there exists at least one unfulfilled request for the subscription, false
   * otherwise.
   */
  function pendingRequestExists(uint256 subId) external view returns (bool);

  /**
   * @notice Paginate through all active VRF subscriptions.
   * @param startIndex index of the subscription to start from
   * @param maxCount maximum number of subscriptions to return, 0 to return all
   * @dev the order of IDs in the list is **not guaranteed**, therefore, if making successive calls, one
   * @dev should consider keeping the blockheight constant to ensure a holistic picture of the contract state
   */
  function getActiveSubscriptionIds(uint256 startIndex, uint256 maxCount) external view returns (uint256[] memory);

  /**
   * @notice Fund a subscription with native.
   * @param subId - ID of the subscription
   * @notice This method expects msg.value to be greater than or equal to 0.
   */
  function fundSubscriptionWithNative(uint256 subId) external payable;
}

// lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol

// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol)

/**
 * @dev Contract module that helps prevent reentrant calls to a function.
 *
 * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
 * available, which can be applied to functions to make sure there are no nested
 * (reentrant) calls to them.
 *
 * Note that because there is a single `nonReentrant` guard, functions marked as
 * `nonReentrant` may not call one another. This can be worked around by making
 * those functions `private`, and then adding `external` `nonReentrant` entry
 * points to them.
 *
 * TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at,
 * consider using {ReentrancyGuardTransient} instead.
 *
 * TIP: If you would like to learn more about reentrancy and alternative ways
 * to protect against it, check out our blog post
 * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
 */
abstract contract ReentrancyGuard {
    // Booleans are more expensive than uint256 or any type that takes up a full
    // word because each write operation emits an extra SLOAD to first read the
    // slot's contents, replace the bits taken up by the boolean, and then write
    // back. This is the compiler's defense against contract upgrades and
    // pointer aliasing, and it cannot be disabled.

    // The values being non-zero value makes deployment a bit more expensive,
    // but in exchange the refund on every call to nonReentrant will be lower in
    // amount. Since refunds are capped to a percentage of the total
    // transaction's gas, it is best to keep them low in cases like this one, to
    // increase the likelihood of the full refund coming into effect.
    uint256 private constant NOT_ENTERED = 1;
    uint256 private constant ENTERED = 2;

    uint256 private _status;

    /**
     * @dev Unauthorized reentrant call.
     */
    error ReentrancyGuardReentrantCall();

    constructor() {
        _status = NOT_ENTERED;
    }

    /**
     * @dev Prevents a contract from calling itself, directly or indirectly.
     * Calling a `nonReentrant` function from another `nonReentrant`
     * function is not supported. It is possible to prevent this from happening
     * by making the `nonReentrant` function external, and making it call a
     * `private` function that does the actual work.
     */
    modifier nonReentrant() {
        _nonReentrantBefore();
        _;
        _nonReentrantAfter();
    }

    function _nonReentrantBefore() private {
        // On the first call to nonReentrant, _status will be NOT_ENTERED
        if (_status == ENTERED) {
            revert ReentrancyGuardReentrantCall();
        }

        // Any calls to nonReentrant after this point will fail
        _status = ENTERED;
    }

    function _nonReentrantAfter() private {
        // By storing the original value once again, a refund is triggered (see
        // https://eips.ethereum.org/EIPS/eip-2200)
        _status = NOT_ENTERED;
    }

    /**
     * @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a
     * `nonReentrant` function in the call stack.
     */
    function _reentrancyGuardEntered() internal view returns (bool) {
        return _status == ENTERED;
    }
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol

// End consumer library.
library VRFV2PlusClient {
  // extraArgs will evolve to support new features
  bytes4 public constant EXTRA_ARGS_V1_TAG = bytes4(keccak256("VRF ExtraArgsV1"));
  struct ExtraArgsV1 {
    bool nativePayment;
  }

  struct RandomWordsRequest {
    bytes32 keyHash;
    uint256 subId;
    uint16 requestConfirmations;
    uint32 callbackGasLimit;
    uint32 numWords;
    bytes extraArgs;
  }

  function _argsToBytes(ExtraArgsV1 memory extraArgs) internal pure returns (bytes memory bts) {
    return abi.encodeWithSelector(EXTRA_ARGS_V1_TAG, extraArgs);
  }
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/access/ConfirmedOwnerWithProposal.sol

/// @title The ConfirmedOwner contract
/// @notice A contract with helpers for basic contract ownership.
contract ConfirmedOwnerWithProposal is IOwnable {
  address private s_owner;
  address private s_pendingOwner;

  event OwnershipTransferRequested(address indexed from, address indexed to);
  event OwnershipTransferred(address indexed from, address indexed to);

  constructor(address newOwner, address pendingOwner) {
    // solhint-disable-next-line gas-custom-errors
    require(newOwner != address(0), "Cannot set owner to zero");

    s_owner = newOwner;
    if (pendingOwner != address(0)) {
      _transferOwnership(pendingOwner);
    }
  }

  /// @notice Allows an owner to begin transferring ownership to a new address.
  function transferOwnership(address to) public override onlyOwner {
    _transferOwnership(to);
  }

  /// @notice Allows an ownership transfer to be completed by the recipient.
  function acceptOwnership() external override {
    // solhint-disable-next-line gas-custom-errors
    require(msg.sender == s_pendingOwner, "Must be proposed owner");

    address oldOwner = s_owner;
    s_owner = msg.sender;
    s_pendingOwner = address(0);

    emit OwnershipTransferred(oldOwner, msg.sender);
  }

  /// @notice Get the current owner
  function owner() public view override returns (address) {
    return s_owner;
  }

  /// @notice validate, transfer ownership, and emit relevant events
  function _transferOwnership(address to) private {
    // solhint-disable-next-line gas-custom-errors
    require(to != msg.sender, "Cannot transfer to self");

    s_pendingOwner = to;

    emit OwnershipTransferRequested(s_owner, to);
  }

  /// @notice validate access
  function _validateOwnership() internal view {
    // solhint-disable-next-line gas-custom-errors
    require(msg.sender == s_owner, "Only callable by owner");
  }

  /// @notice Reverts if called by anyone other than the contract owner.
  modifier onlyOwner() {
    _validateOwnership();
    _;
  }
}

// lib/openzeppelin-contracts/contracts/utils/Pausable.sol

// OpenZeppelin Contracts (last updated v5.3.0) (utils/Pausable.sol)

/**
 * @dev Contract module which allows children to implement an emergency stop
 * mechanism that can be triggered by an authorized account.
 *
 * This module is used through inheritance. It will make available the
 * modifiers `whenNotPaused` and `whenPaused`, which can be applied to
 * the functions of your contract. Note that they will not be pausable by
 * simply including this module, only once the modifiers are put in place.
 */
abstract contract Pausable is Context {
    bool private _paused;

    /**
     * @dev Emitted when the pause is triggered by `account`.
     */
    event Paused(address account);

    /**
     * @dev Emitted when the pause is lifted by `account`.
     */
    event Unpaused(address account);

    /**
     * @dev The operation failed because the contract is paused.
     */
    error EnforcedPause();

    /**
     * @dev The operation failed because the contract is not paused.
     */
    error ExpectedPause();

    /**
     * @dev Modifier to make a function callable only when the contract is not paused.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    modifier whenNotPaused() {
        _requireNotPaused();
        _;
    }

    /**
     * @dev Modifier to make a function callable only when the contract is paused.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    modifier whenPaused() {
        _requirePaused();
        _;
    }

    /**
     * @dev Returns true if the contract is paused, and false otherwise.
     */
    function paused() public view virtual returns (bool) {
        return _paused;
    }

    /**
     * @dev Throws if the contract is paused.
     */
    function _requireNotPaused() internal view virtual {
        if (paused()) {
            revert EnforcedPause();
        }
    }

    /**
     * @dev Throws if the contract is not paused.
     */
    function _requirePaused() internal view virtual {
        if (!paused()) {
            revert ExpectedPause();
        }
    }

    /**
     * @dev Triggers stopped state.
     *
     * Requirements:
     *
     * - The contract must not be paused.
     */
    function _pause() internal virtual whenNotPaused {
        _paused = true;
        emit Paused(_msgSender());
    }

    /**
     * @dev Returns to normal state.
     *
     * Requirements:
     *
     * - The contract must be paused.
     */
    function _unpause() internal virtual whenPaused {
        _paused = false;
        emit Unpaused(_msgSender());
    }
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/shared/access/ConfirmedOwner.sol

/// @title The ConfirmedOwner contract
/// @notice A contract with helpers for basic contract ownership.
contract ConfirmedOwner is ConfirmedOwnerWithProposal {
  constructor(address newOwner) ConfirmedOwnerWithProposal(newOwner, address(0)) {}
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/interfaces/IVRFCoordinatorV2Plus.sol

// Interface that enables consumers of VRFCoordinatorV2Plus to be future-proof for upgrades
// This interface is supported by subsequent versions of VRFCoordinatorV2Plus
interface IVRFCoordinatorV2Plus is IVRFSubscriptionV2Plus {
  /**
   * @notice Request a set of random words.
   * @param req - a struct containing following fields for randomness request:
   * keyHash - Corresponds to a particular oracle job which uses
   * that key for generating the VRF proof. Different keyHash's have different gas price
   * ceilings, so you can select a specific one to bound your maximum per request cost.
   * subId  - The ID of the VRF subscription. Must be funded
   * with the minimum subscription balance required for the selected keyHash.
   * requestConfirmations - How many blocks you'd like the
   * oracle to wait before responding to the request. See SECURITY CONSIDERATIONS
   * for why you may want to request more. The acceptable range is
   * [minimumRequestBlockConfirmations, 200].
   * callbackGasLimit - How much gas you'd like to receive in your
   * fulfillRandomWords callback. Note that gasleft() inside fulfillRandomWords
   * may be slightly less than this amount because of gas used calling the function
   * (argument decoding etc.), so you may need to request slightly more than you expect
   * to have inside fulfillRandomWords. The acceptable range is
   * [0, maxGasLimit]
   * numWords - The number of uint256 random values you'd like to receive
   * in your fulfillRandomWords callback. Note these numbers are expanded in a
   * secure way by the VRFCoordinator from a single random value supplied by the oracle.
   * extraArgs - abi-encoded extra args
   * @return requestId - A unique identifier of the request. Can be used to match
   * a request to a response in fulfillRandomWords.
   */
  function requestRandomWords(VRFV2PlusClient.RandomWordsRequest calldata req) external returns (uint256 requestId);
}

// lib/chainlink-brownie-contracts/contracts/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol

/** ****************************************************************************
 * @notice Interface for contracts using VRF randomness
 * *****************************************************************************
 * @dev PURPOSE
 *
 * @dev Reggie the Random Oracle (not his real job) wants to provide randomness
 * @dev to Vera the verifier in such a way that Vera can be sure he's not
 * @dev making his output up to suit himself. Reggie provides Vera a public key
 * @dev to which he knows the secret key. Each time Vera provides a seed to
 * @dev Reggie, he gives back a value which is computed completely
 * @dev deterministically from the seed and the secret key.
 *
 * @dev Reggie provides a proof by which Vera can verify that the output was
 * @dev correctly computed once Reggie tells it to her, but without that proof,
 * @dev the output is indistinguishable to her from a uniform random sample
 * @dev from the output space.
 *
 * @dev The purpose of this contract is to make it easy for unrelated contracts
 * @dev to talk to Vera the verifier about the work Reggie is doing, to provide
 * @dev simple access to a verifiable source of randomness. It ensures 2 things:
 * @dev 1. The fulfillment came from the VRFCoordinatorV2Plus.
 * @dev 2. The consumer contract implements fulfillRandomWords.
 * *****************************************************************************
 * @dev USAGE
 *
 * @dev Calling contracts must inherit from VRFConsumerBaseV2Plus, and can
 * @dev initialize VRFConsumerBaseV2Plus's attributes in their constructor as
 * @dev shown:
 *
 * @dev   contract VRFConsumerV2Plus is VRFConsumerBaseV2Plus {
 * @dev     constructor(<other arguments>, address _vrfCoordinator, address _subOwner)
 * @dev       VRFConsumerBaseV2Plus(_vrfCoordinator, _subOwner) public {
 * @dev         <initialization with other arguments goes here>
 * @dev       }
 * @dev   }
 *
 * @dev The oracle will have given you an ID for the VRF keypair they have
 * @dev committed to (let's call it keyHash). Create a subscription, fund it
 * @dev and your consumer contract as a consumer of it (see VRFCoordinatorInterface
 * @dev subscription management functions).
 * @dev Call requestRandomWords(keyHash, subId, minimumRequestConfirmations,
 * @dev callbackGasLimit, numWords, extraArgs),
 * @dev see (IVRFCoordinatorV2Plus for a description of the arguments).
 *
 * @dev Once the VRFCoordinatorV2Plus has received and validated the oracle's response
 * @dev to your request, it will call your contract's fulfillRandomWords method.
 *
 * @dev The randomness argument to fulfillRandomWords is a set of random words
 * @dev generated from your requestId and the blockHash of the request.
 *
 * @dev If your contract could have concurrent requests open, you can use the
 * @dev requestId returned from requestRandomWords to track which response is associated
 * @dev with which randomness request.
 * @dev See "SECURITY CONSIDERATIONS" for principles to keep in mind,
 * @dev if your contract could have multiple requests in flight simultaneously.
 *
 * @dev Colliding `requestId`s are cryptographically impossible as long as seeds
 * @dev differ.
 *
 * *****************************************************************************
 * @dev SECURITY CONSIDERATIONS
 *
 * @dev A method with the ability to call your fulfillRandomness method directly
 * @dev could spoof a VRF response with any random value, so it's critical that
 * @dev it cannot be directly called by anything other than this base contract
 * @dev (specifically, by the VRFConsumerBaseV2Plus.rawFulfillRandomness method).
 *
 * @dev For your users to trust that your contract's random behavior is free
 * @dev from malicious interference, it's best if you can write it so that all
 * @dev behaviors implied by a VRF response are executed *during* your
 * @dev fulfillRandomness method. If your contract must store the response (or
 * @dev anything derived from it) and use it later, you must ensure that any
 * @dev user-significant behavior which depends on that stored value cannot be
 * @dev manipulated by a subsequent VRF request.
 *
 * @dev Similarly, both miners and the VRF oracle itself have some influence
 * @dev over the order in which VRF responses appear on the blockchain, so if
 * @dev your contract could have multiple VRF requests in flight simultaneously,
 * @dev you must ensure that the order in which the VRF responses arrive cannot
 * @dev be used to manipulate your contract's user-significant behavior.
 *
 * @dev Since the block hash of the block which contains the requestRandomness
 * @dev call is mixed into the input to the VRF *last*, a sufficiently powerful
 * @dev miner could, in principle, fork the blockchain to evict the block
 * @dev containing the request, forcing the request to be included in a
 * @dev different block with a different hash, and therefore a different input
 * @dev to the VRF. However, such an attack would incur a substantial economic
 * @dev cost. This cost scales with the number of blocks the VRF oracle waits
 * @dev until it calls responds to a request. It is for this reason that
 * @dev that you can signal to an oracle you'd like them to wait longer before
 * @dev responding to the request (however this is not enforced in the contract
 * @dev and so remains effective only in the case of unmodified oracle software).
 */
abstract contract VRFConsumerBaseV2Plus is IVRFMigratableConsumerV2Plus, ConfirmedOwner {
  error OnlyCoordinatorCanFulfill(address have, address want);
  error OnlyOwnerOrCoordinator(address have, address owner, address coordinator);
  error ZeroAddress();

  // s_vrfCoordinator should be used by consumers to make requests to vrfCoordinator
  // so that coordinator reference is updated after migration
  IVRFCoordinatorV2Plus public s_vrfCoordinator;

  /**
   * @param _vrfCoordinator address of VRFCoordinator contract
   */
  constructor(address _vrfCoordinator) ConfirmedOwner(msg.sender) {
    if (_vrfCoordinator == address(0)) {
      revert ZeroAddress();
    }
    s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator);
  }

  /**
   * @notice fulfillRandomness handles the VRF response. Your contract must
   * @notice implement it. See "SECURITY CONSIDERATIONS" above for important
   * @notice principles to keep in mind when implementing your fulfillRandomness
   * @notice method.
   *
   * @dev VRFConsumerBaseV2Plus expects its subcontracts to have a method with this
   * @dev signature, and will call it once it has verified the proof
   * @dev associated with the randomness. (It is triggered via a call to
   * @dev rawFulfillRandomness, below.)
   *
   * @param requestId The Id initially returned by requestRandomness
   * @param randomWords the VRF output expanded to the requested number of words
   */
  // solhint-disable-next-line chainlink-solidity/prefix-internal-functions-with-underscore
  function fulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) internal virtual;

  // rawFulfillRandomness is called by VRFCoordinator when it receives a valid VRF
  // proof. rawFulfillRandomness then calls fulfillRandomness, after validating
  // the origin of the call
  function rawFulfillRandomWords(uint256 requestId, uint256[] calldata randomWords) external {
    if (msg.sender != address(s_vrfCoordinator)) {
      revert OnlyCoordinatorCanFulfill(msg.sender, address(s_vrfCoordinator));
    }
    fulfillRandomWords(requestId, randomWords);
  }

  /**
   * @inheritdoc IVRFMigratableConsumerV2Plus
   */
  function setCoordinator(address _vrfCoordinator) external override onlyOwnerOrCoordinator {
    if (_vrfCoordinator == address(0)) {
      revert ZeroAddress();
    }
    s_vrfCoordinator = IVRFCoordinatorV2Plus(_vrfCoordinator);

    emit CoordinatorSet(_vrfCoordinator);
  }

  modifier onlyOwnerOrCoordinator() {
    if (msg.sender != owner() && msg.sender != address(s_vrfCoordinator)) {
      revert OnlyOwnerOrCoordinator(msg.sender, owner(), address(s_vrfCoordinator));
    }
    _;
  }
}

// contracts/PondManager.sol

// Removed Ownable - using VRFConsumerBaseV2Plus's built-in ownership

interface IPEPETUALCore {
    function balanceOf(address account) external view returns (uint256);
    function totalSupply() external view returns (uint256);
    function excludedFromRewards(address account) external view returns (bool);
}

contract PondManager is ReentrancyGuard, Pausable, VRFConsumerBaseV2Plus {

    // ==================== Pond Configuration ====================
    
    struct PondTier {
        string name;
        uint256 usdValue;        // Target USD value (with 18 decimals)
        uint256 pepeTarget;      // Calculated PEPE amount based on current price
        uint256 accumulated;     // Current PEPE accumulated in this tier
        uint256 completions;     // How many times this tier has been won
        bool active;             // Is this tier currently filling
    }

    PondTier[9] public pondTiers;
    uint256 public currentPondTier = 0;

    // ==================== Core Integration ====================

    IPEPETUALCore public immutable pepetualCore;
    IERC20 public PEPE;

    // ==================== Chainlink Price Oracle ====================

    AggregatorV3Interface public pepePriceFeed;
    uint8 public priceFeedDecimals;
    uint256 public lastPriceUpdate;
    uint256 public priceUpdateInterval = 3600; // 1 hour default
    uint256 public lastPepePrice; // Cached price in USD (8 decimals from Chainlink)

    modifier onlyCore() {
        require(msg.sender == address(pepetualCore), "Only PEPETUAL core");
        _;
    }

    // ==================== VRF Configuration ====================
    
    uint256 public s_subscriptionId;
    bytes32 public s_keyHash;
    uint32 public s_callbackGasLimit = 100000;
    uint16 public s_requestConfirmations = 3;
    uint32 public s_numWords = 1;

    struct PondDraw {
        uint256 tier;
        uint256 prizeAmount;
        address[] eligibleHolders;
        uint256 timestamp;
        bool fulfilled;
        string tierName;
    }

    mapping(uint256 => PondDraw) public pendingDraws;
    uint256[] public activeRequestIds;

    // ==================== Eligible Holder Management ====================

    uint256 public minBalanceForPond = 100 * 10**18; // 100 tokens minimum
    uint256 public maxHoldersPerDraw = 1000; // Gas limit protection

    // Snapshot-based eligible holders
    address[] public eligibleHolders;
    bool public snapshotRequired = false;
    uint256 public snapshotThresholdPercent = 90; // Trigger snapshot at 90% full
    address public keeperAddress; // Optional Chainlink keeper
    uint256 public lastSnapshotBlock;
    uint256 public lastSnapshotTimestamp;

    // ==================== Winners & History ====================
    
    struct PondWinner {
        address winner;
        uint256 tier;
        string tierName;
        uint256 prizeAmount;
        uint256 timestamp;
        uint256 tierCompletionNumber;
    }

    PondWinner[] public pondHistory;
    mapping(address => uint256) public userPondWins; // Total PEPE won by user
    mapping(address => uint256) public userWinCount; // Number of times user won

    uint256 public totalPondWins;
    uint256 public totalPondValue;

    // ==================== Events ====================
    
    event PondFilling(uint256 indexed tier, string tierName, uint256 accumulated, uint256 target);
    event DrawRequested(uint256 indexed requestId, uint256 indexed tier, uint256 eligibleHolders);
    event PondWinnerSelected(address indexed winner, uint256 indexed tier, uint256 prizeAmount, string tierName, uint256 completionNumber);
    event TierAdvanced(uint256 indexed newTier, string newTierName, uint256 targetAmount);
    event PondCarryOver(uint256 indexed nextTier, uint256 amount);
    event PondPrizeReserved(uint256 indexed tier, uint256 amount);
    event PepeTargetsUpdated(uint256 pepePrice);
    event EligibleHoldersUpdated(uint256 count);
    event PondPaused(uint256 tier, uint256 accumulated);
    event PondUnpaused(uint256 tier);
    event SnapshotNeeded(uint256 indexed tier, string tierName, uint256 accumulated, uint256 target);
    event SnapshotSubmitted(uint256 indexed tier, uint256 holdersCount, address indexed submitter);
    event KeeperAddressUpdated(address indexed oldKeeper, address indexed newKeeper);

    // ==================== Constructor ====================
    
    constructor(
        address pepetualCore_,
        address pepe_,
        address vrfCoordinator_,
        uint256 subscriptionId_,
        bytes32 keyHash_
    ) VRFConsumerBaseV2Plus(vrfCoordinator_) {

        require(pepetualCore_ != address(0), "Invalid core");
        require(pepe_ != address(0), "Invalid PEPE");
        require(vrfCoordinator_ != address(0), "Invalid VRF coordinator");

        pepetualCore = IPEPETUALCore(pepetualCore_);
        PEPE = IERC20(pepe_);

        // VRF Configuration
        s_subscriptionId = subscriptionId_;
        s_keyHash = keyHash_;

        // Initialize pond tiers
        _initializePondTiers();
    }

    // ==================== Pond Tier Initialization ====================
    
    function _initializePondTiers() internal {
        pondTiers[0] = PondTier("Tadpole Pool", 10000 * 10**18, 0, 0, 0, true);
        pondTiers[1] = PondTier("Froglet Pond", 17800 * 10**18, 0, 0, 0, false);
        pondTiers[2] = PondTier("Young Frog Lake", 31684 * 10**18, 0, 0, 0, false);
        pondTiers[3] = PondTier("Adult Frog Basin", 56397 * 10**18, 0, 0, 0, false);
        pondTiers[4] = PondTier("Bull Frog Reservoir", 100387 * 10**18, 0, 0, 0, false);
        pondTiers[5] = PondTier("Pond King's Domain", 178689 * 10**18, 0, 0, 0, false);
        pondTiers[6] = PondTier("Swamp Lord's Realm", 318079 * 10**18, 0, 0, 0, false);
        pondTiers[7] = PondTier("Ancient Marsh", 566178 * 10**18, 0, 0, 0, false);
        pondTiers[8] = PondTier("PEPE GOD OCEAN", 1000000 * 10**18, 0, 0, 0, false);

        // Update initial PEPE targets based on current price
        _updatePepeTargets();
    }

    // ==================== Core Pond Logic ====================
    
    function addToPond(uint256 pepeAmount) external onlyCore whenNotPaused {
        require(pepeAmount > 0, "No PEPE to add");

        // Auto-update price targets if needed
        _updatePepeTargets();

        PondTier storage tier = pondTiers[currentPondTier];
        tier.accumulated += pepeAmount;

        emit PondFilling(currentPondTier, tier.name, tier.accumulated, tier.pepeTarget);

        // Check if snapshot needed (at 90% full)
        if (!snapshotRequired && tier.pepeTarget > 0) {
            uint256 thresholdAmount = (tier.pepeTarget * snapshotThresholdPercent) / 100;
            if (tier.accumulated >= thresholdAmount) {
                snapshotRequired = true;
                emit SnapshotNeeded(currentPondTier, tier.name, tier.accumulated, tier.pepeTarget);
            }
        }

        // Check if tier is complete (only trigger if snapshot taken)
        if (tier.accumulated >= tier.pepeTarget && eligibleHolders.length > 0) {
            _triggerDraw();
        }
    }

    function _triggerDraw() internal {
        address[] memory eligible = _getEligibleHolders();
        
        if (eligible.length == 0) {
            // No eligible holders, roll over to next tier
            _rolloverToNextTier();
            return;
        }

        // Request randomness from VRF
        uint256 requestId = s_vrfCoordinator.requestRandomWords(
            VRFV2PlusClient.RandomWordsRequest({
                keyHash: s_keyHash,
                subId: s_subscriptionId,
                requestConfirmations: s_requestConfirmations,
                callbackGasLimit: s_callbackGasLimit,
                numWords: s_numWords,
                extraArgs: VRFV2PlusClient._argsToBytes(
                    VRFV2PlusClient.ExtraArgsV1({nativePayment: false})
                )
            })
        );

        // Store draw data
        PondTier storage tier = pondTiers[currentPondTier];
        uint256 prizeAmount = tier.accumulated;

        pendingDraws[requestId] = PondDraw({
            tier: currentPondTier,
            prizeAmount: prizeAmount,
            eligibleHolders: eligible,
            timestamp: block.timestamp,
            fulfilled: false,
            tierName: tier.name
        });

        if (prizeAmount > 0) {
            tier.accumulated -= prizeAmount;
            emit PondPrizeReserved(currentPondTier, prizeAmount);
        }

        activeRequestIds.push(requestId);

        emit DrawRequested(requestId, currentPondTier, eligible.length);
    }

    function fulfillRandomWords(
        uint256 requestId,
        uint256[] calldata randomWords
    ) internal override {
        PondDraw storage draw = pendingDraws[requestId];
        
        require(!draw.fulfilled, "Draw already fulfilled");
        require(draw.eligibleHolders.length > 0, "No eligible holders");
        require(block.timestamp <= draw.timestamp + 1 hours, "Draw expired");

        // Select winner
        uint256 winnerIndex = randomWords[0] % draw.eligibleHolders.length;
        address winner = draw.eligibleHolders[winnerIndex];

        // Record the win
        PondTier storage tier = pondTiers[draw.tier];
        uint256 completionNumber = tier.completions + 1;

        pondHistory.push(PondWinner({
            winner: winner,
            tier: draw.tier,
            tierName: draw.tierName,
            prizeAmount: draw.prizeAmount,
            timestamp: block.timestamp,
            tierCompletionNumber: completionNumber
        }));

        // Update winner stats
        userPondWins[winner] += draw.prizeAmount;
        userWinCount[winner]++;
        totalPondWins++;
        totalPondValue += draw.prizeAmount;

        // Send prize
        PEPE.transfer(winner, draw.prizeAmount);

        // Count completed tier only when prize is delivered
        tier.completions = completionNumber;

        // Mark as fulfilled
        draw.fulfilled = true;

        emit PondWinnerSelected(winner, draw.tier, draw.prizeAmount, draw.tierName, completionNumber);

        // Advance to next tier
        _advanceToNextTier();

        // Clean up request ID
        _removeActiveRequest(requestId);
    }

    function _advanceToNextTier() internal {
        PondTier storage completedTier = pondTiers[currentPondTier];
        uint256 carryOver = completedTier.accumulated;

        // Reset tier state for the next cycle
        completedTier.accumulated = 0;
        completedTier.active = false;

        uint256 nextTier = currentPondTier + 1;
        if (nextTier >= pondTiers.length) {
            nextTier = pondTiers.length - 1; // stay on top tier once reached
        }

        currentPondTier = nextTier;

        // Activate new tier (re-activate same tier if staying on the top)
        PondTier storage nextTierRef = pondTiers[currentPondTier];
        nextTierRef.active = true;

        if (carryOver > 0) {
            nextTierRef.accumulated += carryOver;
            emit PondCarryOver(currentPondTier, carryOver);
        }

        // Reset snapshot for new tier
        delete eligibleHolders;
        snapshotRequired = false;

        emit TierAdvanced(currentPondTier, pondTiers[currentPondTier].name, pondTiers[currentPondTier].pepeTarget);
    }

    function _rolloverToNextTier() internal {
        _advanceToNextTier();
    }

    // ==================== Eligible Holder Management ====================

    function _getEligibleHolders() internal view returns (address[] memory) {
        uint256 count = eligibleHolders.length;
        if (count == 0) return new address[](0);

        // Limit to maxHoldersPerDraw to prevent gas issues
        if (count > maxHoldersPerDraw) {
            count = maxHoldersPerDraw;
        }

        address[] memory eligible = new address[](count);
        for (uint256 i = 0; i < count; i++) {
            eligible[i] = eligibleHolders[i];
        }

        return eligible;
    }

    // Submit snapshot of eligible holders (can be called by owner or keeper)
    function submitSnapshot(address[] calldata holders) external {
        require(
            msg.sender == owner() || msg.sender == keeperAddress,
            "Not authorized"
        );
        require(snapshotRequired, "Snapshot not needed");
        require(holders.length > 0, "No holders provided");
        require(holders.length <= maxHoldersPerDraw, "Too many holders");

        // Clear old snapshot
        delete eligibleHolders;

        // Set new snapshot
        for (uint256 i = 0; i < holders.length; i++) {
            eligibleHolders.push(holders[i]);
        }

        snapshotRequired = false;
        lastSnapshotBlock = block.number;
        lastSnapshotTimestamp = block.timestamp;

        emit SnapshotSubmitted(currentPondTier, holders.length, msg.sender);

        // Auto-trigger draw if pond is full
        PondTier storage tier = pondTiers[currentPondTier];
        if (tier.accumulated >= tier.pepeTarget) {
            _triggerDraw();
        }
    }

    // Legacy function kept for interface compatibility
    function updateHolderEligibility(address, uint256) external onlyCore {
        // No longer tracking individual updates - using snapshots instead
    }

    // ==================== Price Oracle Integration ====================

    function _updatePepeTargets() internal {
        // Only update if enough time has passed or never updated
        if (block.timestamp < lastPriceUpdate + priceUpdateInterval && lastPriceUpdate != 0) {
            return;
        }

        if (address(pepePriceFeed) == address(0)) {
            // No price feed set, use fixed targets (fallback)
            return;
        }

        uint8 decimals_ = priceFeedDecimals;
        if (decimals_ == 0) {
            try pepePriceFeed.decimals() returns (uint8 dec) {
                decimals_ = dec;
                priceFeedDecimals = dec;
            } catch {
                return;
            }
        }

        uint256 priceScale = 10 ** uint256(decimals_);

        try pepePriceFeed.latestRoundData() returns (
            uint80,
            int256 price,
            uint256,
            uint256 updatedAt,
            uint80
        ) {
            require(price > 0, "Invalid price");
            require(updatedAt > 0, "Stale price feed");

            uint256 pepePrice = uint256(price);
            lastPepePrice = pepePrice;
            lastPriceUpdate = block.timestamp;

            // Update all tier targets based on USD values
            for (uint256 i = 0; i < pondTiers.length; i++) {
                pondTiers[i].pepeTarget = (pondTiers[i].usdValue * priceScale) / pepePrice;
            }

            emit PepeTargetsUpdated(pepePrice);
        } catch {
            // Price feed failed, keep existing targets
        }
    }

    function updatePepeTargets() external {
        _updatePepeTargets();
    }

    function forceUpdatePepeTargets() external onlyOwner {
        lastPriceUpdate = 0; // Reset to force update
        _updatePepeTargets();
    }

    // ==================== View Functions ====================
    
    function getCurrentTierInfo() external view returns (
        uint256 tierIndex,
        string memory tierName,
        uint256 accumulated,
        uint256 target,
        uint256 progressPercent,
        uint256 usdValue
    ) {
        PondTier storage tier = pondTiers[currentPondTier];
        return (
            currentPondTier,
            tier.name,
            tier.accumulated,
            tier.pepeTarget,
            tier.pepeTarget > 0 ? (tier.accumulated * 100) / tier.pepeTarget : 0,
            tier.usdValue
        );
    }

    function getTierInfo(uint256 tierIndex) external view returns (
        string memory name,
        uint256 usdValue,
        uint256 pepeTarget,
        uint256 accumulated,
        uint256 completions,
        bool active
    ) {
        require(tierIndex < pondTiers.length, "Invalid tier");
        PondTier storage tier = pondTiers[tierIndex];
        return (tier.name, tier.usdValue, tier.pepeTarget, tier.accumulated, tier.completions, tier.active);
    }

    function getAllTiersInfo() external view returns (PondTier[] memory) {
        PondTier[] memory tiers = new PondTier[](pondTiers.length);
        for (uint256 i = 0; i < pondTiers.length; i++) {
            tiers[i] = pondTiers[i];
        }
        return tiers;
    }

    function getUserPondStats(address user) external view returns (
        uint256 totalWinnings,
        uint256 winCount,
        bool isEligible
    ) {
        return (
            userPondWins[user],
            userWinCount[user],
            _isEligibleHolder(user)
        );
    }

    function _isEligibleHolder(address holder) internal view returns (bool) {
        return pepetualCore.balanceOf(holder) >= minBalanceForPond && 
               !pepetualCore.excludedFromRewards(holder);
    }

    function getRecentWinners(uint256 count) external view returns (PondWinner[] memory) {
        uint256 totalWins = pondHistory.length;
        if (count > totalWins) count = totalWins;
        if (count == 0) return new PondWinner[](0);

        PondWinner[] memory winners = new PondWinner[](count);
        for (uint256 i = 0; i < count; i++) {
            winners[i] = pondHistory[totalWins - 1 - i];
        }
        return winners;
    }

    function getPendingDraws() external view returns (uint256[] memory) {
        return activeRequestIds;
    }

    function getSnapshotInfo() external view returns (
        bool required,
        uint256 threshold,
        uint256 holdersCount,
        uint256 lastBlock,
        uint256 lastTimestamp,
        address keeper
    ) {
        return (
            snapshotRequired,
            snapshotThresholdPercent,
            eligibleHolders.length,
            lastSnapshotBlock,
            lastSnapshotTimestamp,
            keeperAddress
        );
    }

    function getEligibleHolders() external view returns (address[] memory) {
        return eligibleHolders;
    }

    function getOracleInfo() external view returns (
        address priceFeed,
        uint256 currentPrice,
        uint256 lastUpdate,
        uint256 updateInterval,
        bool needsUpdate
    ) {
        return (
            address(pepePriceFeed),
            lastPepePrice,
            lastPriceUpdate,
            priceUpdateInterval,
            block.timestamp >= lastPriceUpdate + priceUpdateInterval
        );
    }

    // ==================== Admin Functions ====================

    function setPriceFeed(address newPriceFeed) external onlyOwner {
        pepePriceFeed = AggregatorV3Interface(newPriceFeed);
        if (newPriceFeed != address(0)) {
            try pepePriceFeed.decimals() returns (uint8 dec) {
                priceFeedDecimals = dec;
            } catch {
                priceFeedDecimals = 0;
            }
            lastPriceUpdate = 0; // Force update on next call
            _updatePepeTargets();
        } else {
            priceFeedDecimals = 0;
        }
    }

    function setPriceUpdateInterval(uint256 newInterval) external onlyOwner {
        require(newInterval >= 300, "Minimum 5 minutes");
        require(newInterval <= 86400, "Maximum 24 hours");
        priceUpdateInterval = newInterval;
    }

    function setMinBalanceForPond(uint256 newMinBalance) external onlyOwner {
        minBalanceForPond = newMinBalance;
    }

    function setMaxHoldersPerDraw(uint256 newMax) external onlyOwner {
        require(newMax > 0 && newMax <= 10000, "Invalid max holders");
        maxHoldersPerDraw = newMax;
    }

    function setKeeperAddress(address newKeeper) external onlyOwner {
        address oldKeeper = keeperAddress;
        keeperAddress = newKeeper;
        emit KeeperAddressUpdated(oldKeeper, newKeeper);
    }

    function setSnapshotThreshold(uint256 newThreshold) external onlyOwner {
        require(newThreshold >= 50 && newThreshold <= 100, "Invalid threshold");
        snapshotThresholdPercent = newThreshold;
    }

    function setPepeToken(address newPepe) external onlyOwner {
        require(newPepe != address(0), "Invalid PEPE address");
        PEPE = IERC20(newPepe);
    }

    function forceSnapshot() external onlyOwner {
        snapshotRequired = true;
        PondTier storage tier = pondTiers[currentPondTier];
        emit SnapshotNeeded(currentPondTier, tier.name, tier.accumulated, tier.pepeTarget);
    }

    function updateVRFConfig(
        uint256 subscriptionId,
        bytes32 keyHash,
        uint32 callbackGasLimit,
        uint16 requestConfirmations
    ) external onlyOwner {
        s_subscriptionId = subscriptionId;
        s_keyHash = keyHash;
        s_callbackGasLimit = callbackGasLimit;
        s_requestConfirmations = requestConfirmations;
    }

    function manualAdvanceTier() external onlyOwner {
        _advanceToNextTier();
    }

    function emergencyWithdraw() external onlyOwner {
        uint256 balance = PEPE.balanceOf(address(this));
        PEPE.transfer(owner(), balance);
    }

    function pause() external onlyOwner {
        _pause();
        emit PondPaused(currentPondTier, pondTiers[currentPondTier].accumulated);
    }

    function unpause() external onlyOwner {
        _unpause();
        emit PondUnpaused(currentPondTier);
    }

    // ==================== Helper Functions ====================
    
    function _removeActiveRequest(uint256 requestId) internal {
        for (uint256 i = 0; i < activeRequestIds.length; i++) {
            if (activeRequestIds[i] == requestId) {
                activeRequestIds[i] = activeRequestIds[activeRequestIds.length - 1];
                activeRequestIds.pop();
                break;
            }
        }
    }

    // Emergency function to clean up expired draws
    function cleanupExpiredDraws() external {
        for (uint256 i = 0; i < activeRequestIds.length; i++) {
            uint256 requestId = activeRequestIds[i];
            PondDraw storage draw = pendingDraws[requestId];

            if (!draw.fulfilled && block.timestamp > draw.timestamp + 1 hours) {
                PondTier storage tier = pondTiers[draw.tier];

                // Restore reserved prize so it can be redrawn alongside any carry-over
                tier.accumulated += draw.prizeAmount;

                draw.fulfilled = true;

                // Mark that a fresh snapshot is needed so holders can be re-submitted
                if (draw.tier == currentPondTier) {
                    snapshotRequired = true;
                    emit SnapshotNeeded(draw.tier, tier.name, tier.accumulated, tier.pepeTarget);
                }

                // Remove from active requests
                _removeActiveRequest(requestId);
                i--; // Adjust index after removal
            }
        }
    }

    receive() external payable {}
}

Tags:
ERC20, Multisig, Pausable, Multi-Signature, Factory, Oracle|addr:0x63edc873e88f1222777c52cbbea75663e6e6cd42|verified:true|block:23454297|tx:0xea7bb88b42a1cec58873721ae3b937bf6848782ff7b850de2f2ade533cfa8e55|first_check:1758979277

Submitted on: 2025-09-27 15:21:19

Comments

Log in to comment.

No comments yet.