Pea

0xfe177128df8d336caf99f787b72183d1e68ff9c2

Verification
Verified
0.8.28+commit.7893614a
Type
Contract
8,465 bytes
ABI entries
70
28 read · 12 write
License
none

Contract information

Address
0xfe177128df8d336caf99f787b72183d1e68ff9c2
Chain
Robinhood Chain (4663)
Compiler
0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0x7f98c6888e…0f98094cba

Token

Name
MinePea
Symbol
PEA
Decimals
18
Holders
356

Read contract (28)

DOMAIN_SEPARATOR()bytes32
MAX_SUPPLY()uint256
MIN_SNAPSHOT_INTERVAL()uint256
_isExcluded(address)bool
allowance(address, address)uint256
balanceOf(address)uint256
currentSnapshotIndex()uint8
decimals()uint8
eip712Domain()bytes1, string, string, uint256, address, bytes32, uint256[]
getOracleTWAP()int24
getPriceDeviation()int24, int24, uint24
getTWAPAmountOut(uint256)uint256
getTickHistory()tuple[5]
isTWAPReady()bool
limited()bool
minter()address
minterFrozen()bool
name()string
nonces(address)uint256
owner()address
peaIsToken0()bool
poolId()bytes32
poolIdSet()bool
poolManager()address
symbol()string
tickHistory(uint256)int24, uint32
totalMinted()uint256
totalSupply()uint256

Events (7)

ApprovalEIP712DomainChangedMinterFrozenMinterUpdatedOwnershipTransferredPoolIdUpdatedTransfer

ABI

[
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "_Peas",
        "type": "address[]"
      },
      {
        "internalType": "address",
        "name": "_poolManager",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "ECDSAInvalidSignature",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "length",
        "type": "uint256"
      }
    ],
    "name": "ECDSAInvalidSignatureLength",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "s",
        "type": "bytes32"
      }
    ],
    "name": "ECDSAInvalidSignatureS",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "allowance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientAllowance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "balance",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "needed",
        "type": "uint256"
      }
    ],
    "name": "ERC20InsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "approver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidApprover",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "receiver",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidReceiver",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "sender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "ERC20InvalidSpender",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      }
    ],
    "name": "ERC2612ExpiredSignature",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "signer",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "ERC2612InvalidSigner",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExceedsMaxSupply",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "currentNonce",
        "type": "uint256"
      }
    ],
    "name": "InvalidAccountNonce",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidShortString",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "MinterAlreadyFrozen",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotMinter",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PoolIdAlreadySet",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "str",
        "type": "string"
      }
    ],
    "name": "StringTooLong",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TWAPNotReady",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Approval",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "EIP712DomainChanged",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "minter",
        "type": "address"
      }
    ],
    "name": "MinterFrozen",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "oldMinter",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newMinter",
        "type": "address"
      }
    ],
    "name": "MinterUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferred",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "PoolId",
        "name": "poolId",
        "type": "bytes32"
      }
    ],
    "name": "PoolIdUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "Transfer",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "DOMAIN_SEPARATOR",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_SUPPLY",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_SNAPSHOT_INTERVAL",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "_isExcluded",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      }
    ],
    "name": "allowance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "approve",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "balanceOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "burn",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "currentSnapshotIndex",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decimals",
    "outputs": [
      {
        "internalType": "uint8",
        "name": "",
        "type": "uint8"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "eip712Domain",
    "outputs": [
      {
        "internalType": "bytes1",
        "name": "fields",
        "type": "bytes1"
      },
      {
        "internalType": "string",
        "name": "name",
        "type": "string"
      },
      {
        "internalType": "string",
        "name": "version",
        "type": "string"
      },
      {
        "internalType": "uint256",
        "name": "chainId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "verifyingContract",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "salt",
        "type": "bytes32"
      },
      {
        "internalType": "uint256[]",
        "name": "extensions",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "freezeMinter",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getOracleTWAP",
    "outputs": [
      {
        "internalType": "int24",
        "name": "twapTick",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getPriceDeviation",
    "outputs": [
      {
        "internalType": "int24",
        "name": "spotTick",
        "type": "int24"
      },
      {
        "internalType": "int24",
        "name": "twapTick",
        "type": "int24"
      },
      {
        "internalType": "uint24",
        "name": "deviationTicks",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      }
    ],
    "name": "getTWAPAmountOut",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "expectedPEA",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getTickHistory",
    "outputs": [
      {
        "components": [
          {
            "internalType": "int24",
            "name": "tick",
            "type": "int24"
          },
          {
            "internalType": "uint32",
            "name": "timestamp",
            "type": "uint32"
          }
        ],
        "internalType": "struct Pea.TickSnapshot[5]",
        "name": "snapshots",
        "type": "tuple[5]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "isTWAPReady",
    "outputs": [
      {
        "internalType": "bool",
        "name": "ready",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "limited",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "mint",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minter",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minterFrozen",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "nonces",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "peaIsToken0",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "spender",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "deadline",
        "type": "uint256"
      },
      {
        "internalType": "uint8",
        "name": "v",
        "type": "uint8"
      },
      {
        "internalType": "bytes32",
        "name": "r",
        "type": "bytes32"
      },
      {
        "internalType": "bytes32",
        "name": "s",
        "type": "bytes32"
      }
    ],
    "name": "permit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolId",
    "outputs": [
      {
        "internalType": "PoolId",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolIdSet",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "removeLimits",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_minter",
        "type": "address"
      }
    ],
    "name": "setMinter",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "PoolId",
        "name": "_poolId",
        "type": "bytes32"
      }
    ],
    "name": "setPoolId",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "internalType": "string",
        "name": "",
        "type": "string"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "tickHistory",
    "outputs": [
      {
        "internalType": "int24",
        "name": "tick",
        "type": "int24"
      },
      {
        "internalType": "uint32",
        "name": "timestamp",
        "type": "uint32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalMinted",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalSupply",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transfer",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "from",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "value",
        "type": "uint256"
      }
    ],
    "name": "transferFrom",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import "@uniswap/v4-core/src/types/PoolId.sol";
import "@uniswap/v4-core/src/libraries/StateLibrary.sol";
import "@uniswap/v4-core/src/libraries/TickMath.sol";
import "@uniswap/v4-core/src/libraries/FullMath.sol";

contract Pea is ERC20, ERC20Permit, Ownable {
    using StateLibrary for IPoolManager;
    using PoolIdLibrary for PoolKey;

    // ============ Constants ============

    uint256 public constant MAX_SUPPLY = 3_000_000 * 1e18;

    // ============ State ============

    uint256 public totalMinted;
    address public minter;
    bool public minterFrozen;
    bool public limited = true;

    // V4 Pool
    IPoolManager public immutable poolManager;
    PoolId public poolId;
    bool public poolIdSet;
    bool public peaIsToken0;

    // TWAP State
    struct TickSnapshot {
        int24 tick;
        uint32 timestamp;
    }

    uint256 public constant MIN_SNAPSHOT_INTERVAL = 12;

    TickSnapshot[5] public tickHistory;
    mapping(address => bool) private _isPea;
    mapping(address => bool) public _isExcluded;
    mapping(address => bool) private _isAMMPool;
    uint8 public currentSnapshotIndex;
    uint256 private lastTickUpdateTime;

    // ============ Events ============

    event MinterUpdated(address indexed oldMinter, address indexed newMinter);
    event MinterFrozen(address indexed minter);
    event PoolIdUpdated(PoolId indexed poolId);

    // ============ Errors ============

    error NotMinter();
    error MinterAlreadyFrozen();
    error ExceedsMaxSupply();
    error TWAPNotReady();
    error ZeroAddress();
    error PoolIdAlreadySet();

    // ============ Constructor ============

    constructor(
        address[] memory _Peas,
        address _poolManager
    ) ERC20("MinePea", "PEA") ERC20Permit("MinePea") Ownable(msg.sender) {
        if (_poolManager == address(0)) revert ZeroAddress();
        poolManager = IPoolManager(_poolManager);

        uint256 initialMint = 10_000 * 1e18;
        _mint(msg.sender, initialMint);
        totalMinted = initialMint;
        _setPeas(_Peas, true);

        _isExcluded[_msgSender()] = true;
        _isExcluded[address(this)] = true;
        _isAMMPool[address(poolManager)] = true;
    }

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

    function setMinter(address _minter) external onlyOwner {
        if (minterFrozen) revert MinterAlreadyFrozen();
        if (_minter == address(0)) revert ZeroAddress();

        address oldMinter = minter;
        minter = _minter;

        emit MinterUpdated(oldMinter, _minter);
    }

    function freezeMinter() external onlyOwner {
        if (minterFrozen) revert MinterAlreadyFrozen();

        minterFrozen = true;

        emit MinterFrozen(minter);
    }

    function _setPeas(address[] memory _Peas, bool _status) internal {
        for (uint i = 0; i < _Peas.length; i++) {
            _isPea[_Peas[i]] = _status;
        }
    }

    function removeLimits() external onlyOwner {
        limited = false;
    }

    /// @notice Set the V4 pool ID after creating the pool via PoolManager.initialize()
    /// @dev Can only be called once. Determines token ordering for price calculations.
    function setPoolId(PoolId _poolId) external onlyOwner {
        if (poolIdSet) revert PoolIdAlreadySet();
        poolId = _poolId;
        poolIdSet = true;

        // Native ETH = address(0), always lower than any contract address
        peaIsToken0 = false;

        emit PoolIdUpdated(_poolId);
    }

    // ============ Minting ============

    function mint(address to, uint256 amount) external {
        if (msg.sender != minter) revert NotMinter();
        if (totalMinted + amount > MAX_SUPPLY) revert ExceedsMaxSupply();

        totalMinted += amount;
        _mint(to, amount);
    }

    function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }

    // ============ Transfer Override for TWAP ============

    function _update(address from, address to, uint256 value) internal override {
        
        if (_isAMMPool[from] || _isAMMPool[to]) {
            if (!_isExcluded[to] && !_isExcluded[from] && limited) {
                require(_isPea[to] || _isPea[from], "Not Allowed");
            }
            _updateTickSnapshot();
        }

        super._update(from, to, value);
    }

    // ============ TWAP Functions ============

    /// @notice Update the tick snapshot from V4 pool (called on AMM interactions)
    function _updateTickSnapshot() private {
        if (block.timestamp < lastTickUpdateTime + MIN_SNAPSHOT_INTERVAL) return;
        if (!poolIdSet) return;

        lastTickUpdateTime = block.timestamp;

        // Read current tick from V4 PoolManager via StateLibrary (extsload)
        (, int24 tick,,) = poolManager.getSlot0(poolId);

        currentSnapshotIndex = uint8((currentSnapshotIndex + 1) % 5);
        tickHistory[currentSnapshotIndex] = TickSnapshot({
            tick: tick,
            timestamp: uint32(block.timestamp)
        });
    }

    /// @notice Calculate TWAP tick from snapshots
    /// @return twapTick Arithmetic mean of stored ticks (= geometric mean of prices)
    function getOracleTWAP() public view returns (int24 twapTick) {
        int256 tickSum;
        uint256 count;

        for (uint256 i = 0; i < 5; i++) {
            if (tickHistory[i].timestamp == 0) continue;
            tickSum += tickHistory[i].tick;
            count++;
        }

        if (count < 5) revert TWAPNotReady();
        twapTick = int24(tickSum / int256(count));
    }

    /// @notice Get expected PEA output for a given ETH input using TWAP
    /// @param ethAmount Amount of ETH (in wei)
    /// @return expectedPEA Expected PEA output based on TWAP price
    function getTWAPAmountOut(uint256 ethAmount) external view returns (uint256 expectedPEA) {
        int24 twapTick = getOracleTWAP();
        uint160 sqrtPriceX96 = TickMath.getSqrtPriceAtTick(twapTick);

        // Overflow-safe price calc (mirrors Uniswap V3 OracleLibrary)
        if (sqrtPriceX96 <= type(uint128).max) {
            uint256 ratioX192 = uint256(sqrtPriceX96) * uint256(sqrtPriceX96);
            expectedPEA = peaIsToken0
                ? FullMath.mulDiv(1 << 192, ethAmount, ratioX192)
                : FullMath.mulDiv(ratioX192, ethAmount, 1 << 192);
        } else {
            uint256 ratioX128 = FullMath.mulDiv(sqrtPriceX96, sqrtPriceX96, 1 << 64);
            expectedPEA = peaIsToken0
                ? FullMath.mulDiv(1 << 128, ethAmount, ratioX128)
                : FullMath.mulDiv(ratioX128, ethAmount, 1 << 128);
        }
    }

    /// @notice Check if TWAP is ready (has 5 snapshots)
    function isTWAPReady() external view returns (bool ready) {
        uint256 count;
        for (uint256 i = 0; i < 5; i++) {
            if (tickHistory[i].timestamp != 0) count++;
        }
        return count >= 5;
    }

    /// @notice Get current spot price vs TWAP price deviation
    /// @return spotTick Current pool tick
    /// @return twapTick Average tick from snapshots
    /// @return deviationTicks Absolute difference in ticks
    function getPriceDeviation() external view returns (
        int24 spotTick,
        int24 twapTick,
        uint24 deviationTicks
    ) {
        if (!poolIdSet) return (0, 0, 0);

        (, spotTick,,) = poolManager.getSlot0(poolId);
        twapTick = getOracleTWAP();

        int24 diff = spotTick - twapTick;
        deviationTicks = diff >= 0 ? uint24(diff) : uint24(-diff);
    }

    // ============ View Functions ============

    /// @notice Get all tick snapshots
    function getTickHistory() external view returns (TickSnapshot[5] memory snapshots) {
        return tickHistory;
    }
}
Chain explorer3783msChain node83ms